npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@volter/twin-x

v0.1.36

Published

Local X (Twitter) API v2 twin (post, reply, delete, timelines, user lookup) with an x.com mirror, built on @volter/world-core.

Readme

@volter/twin-x — the X (Twitter) API v2 posting surface

A derived pack over X's own OpenAPI document (X API v2 version 2.168, spec/openapi.json.gz, provenance and corrections in spec/SOURCE.md): the wire is generated (src/generated/), every operation the twin serves is a semantics handler (src/semantics/), and every other operation of the spec answers X's 404 problem, the gap (src/manifest.ts unmodeled). Protocol standing is in the generated index.

The org's public voice: post (with images or a video), quote, reply to a mention, retract, look a post or an account back up, and read the mentions / own / home timelines and recent search — offline, against local state, with X's own tweet.fields projection, bearer/scope gate and problem envelopes.

bun packages/twin/x/src/cli.ts serve --root /tmp/world
# then point any X v2 client at it: X_TWIN_URL=http://127.0.0.1:<port> node --require @volter/world-core/inject app.js

Coverage

| Area | What the twin serves | |---|---| | POST /2/tweets | Create an original post — with reply.in_reply_to_tweet_id a reply, with quote_tweet_id a quote. One route, three body fields. Text over the account's limit (280, or its X Premium long-post entitlement) is refused before anything is written. media.media_ids attaches up to four images or one video this account uploaded; an unknown, foreign, expired or still-processing id is X's Your media IDs are invalid. | | POST /2/media/upload · …/initialize · …/{id}/append · …/{id}/finalize · GET /2/media/upload?command=STATUS | Media upload, needing media.write: an image (tweet_image, dm_image) in one request, multipart or base64 JSON, typed and sized from its own bytes; a video (tweet_video, amplify_video, MP4) in chunks, whose processing_info goes pending → in_progress → succeeded one STATUS read at a time (a file that is not an MP4 fails as InvalidMedia). Bytes live on the kernel's blob seam, content-addressed; an upload is not an entry — the post that attaches it is. GIF, DM video and subtitle categories are refused by name. | | media bytes | Served at X's own path shapes under the twin's base (a World's mount included): /media/<name>.<ext> for an image, /ext_tw_video/<id>/pu/vid/avc1/<w>x<h>/<name>.mp4 for a video with HTTP Range, /ext_tw_video_thumb/… for a video's preview — a stand-in poster, since the twin decodes no frames. | | DELETE /2/tweets/:id | Retract a post. Only your own; only once. | | GET /2/tweets/:id · GET /2/tweets?ids= | Post lookup, single and bulk. The bulk form is partial, as X's is: the ids that exist in data, the ones that do not in errors — never a fabricated row. | | GET /2/users/:id/mentions | The mentions timeline — posts naming this account. | | GET /2/users/:id/tweets | The account's own posted timeline. | | GET /2/users/:id/timelines/reverse_chronological | The home timeline — the account plus the accounts it follows; refused for anyone else's id. | | GET /2/users/:id · GET /2/users/by/username/:username | User lookup (handles are case-insensitive). An unknown user is X's partial-error 200 with a resource_type: "user" row, never a fabricated account. GET /2/users/me is xidentity's and is not claimed here. | | exclude | exclude=replies,retweets on the account's own and home timelines: replies leave; the twin holds no reposts, so excluding them leaves every post. | | GET /2/tweets/search/recent | Recent search over the local corpus: keywords, quoted phrases and from:. Grammar the twin does not model (OR, negation, grouping, other operators) is refused, never silently ignored. | | projection | tweet.fields (or post.fields, the name X's document and its own @xdevplatform/xdk give the same parameter), user.fields, media.fields and expansions (attachments.media_keys included) + the includes sidecar. The default is X's three fields (id, text, edit_history_tweet_ids) and nothing else — a documented field the twin holds no state for is refused by name rather than quietly omitted. user.fields=profile_image_url is the photo seeded for the account, or X's default avatar URL for one without, which the twin serves on abs.twimg.com (below). | | paging + windows | pagination_token (search's next_token too) / meta.next_token, max_results (5..100 on the timelines, 10..100 on search), and since_id / until_id / start_time / end_time with X's own id-beats-time precedence. | | auth | The bearer xidentity's OAuth 2.0 flow mints and its scope set, held to every scope the operation's spec names (its security), or an OAuth 1.0a User Context signature (HMAC-SHA1) over an access token xidentity's three legs issued — a Read-only App's token reads and gets X's oauth1-permissions 403 on a write; X's about:blank 401 / Forbidden 403 problems. | | errors | resource-not-found, invalid-request (a query parameter the operation's spec does not have included), the 429 + legacy code 88 pair, 405 under --read-only. | | the gap | Every other operation of X's spec (180 of 195: DMs, lists, spaces, likes, reposts, bookmarks, streams, counts, full-archive search, …) is X's 404 problem from this twin, never a fabricated success and never forwarded to X. | | real state system | Refresh observes the credential's account (GET /2/users/me) and both of its timelines — mentions and its own posts — following next_token to the end of each; perform resends a post, quote, reply or delete to the real vendor under the id X minted. A post with media is performed by uploading each file to X first (an image in one request; a video chunked, then polled on STATUS until processed) and posting with the media ids X minted. Every call is charged to the credential's rate budget first. | | /_twin/* | The World's doors (src/x-doors.ts): seed an account (its profile — description, location, url, profile_image_url, protected, verified — and its long-post entitlement), a bearer (kept by its SHA-256), an inbound mention, a follow edge, or an armed rate refusal. |

The spec is the denominator of the wire: 195 operations, 15 served by handlers, 180 the gap (xOwners()). src/x-capabilities.ts holds the protocol 2 harness: 139 capabilities, 94 done. What is not modelled is enumerated there as a todo — polls, hide-reply, entities/public_metrics, the user.fields with no state behind them, full-archive search and the query grammar, likes/reposts/bookmarks, stream rules, the filtered and sampled streaming connections, GIFs, alt text, a video's real poster frame and X's transcoded variants — not omitted. One CORE todo remains and says why in its own title: x.rate_limit.published_figures needs X's published per-endpoint posting limits, and this pack makes no X call to fetch them.

One route, two operations, and why that is the whole point

An original post and a reply are the same HTTP operation at X: POST /2/tweets, differing only by reply.in_reply_to_tweet_id in the body. The pinned official client proves it — tweet() posts {text} to tweets and reply() posts {text, reply:{in_reply_to_tweet_id}} to the same tweets ([email protected], dist/cjs/v2/client.v2.write.js:89 and :180). A quote is a third body field on that same route (quote_tweet_id, :187).

That matters beyond fidelity, because the example org graduates the two at different times: replying to a mention is allowed before posting under the org's own name is. A census keyed by method+path names ONE capability for both, and that ladder cannot be written down at all.

So x-spec-census.json declares requestKey: "method+path+body-field" and carries two entries for that one route — x.tweets.reply when the field is present, x.tweets.create when it is absent — both mapped as operationCapability, so a consumer that has not learned to read the body denies rather than granting the first entry to both. Every other route in the census is an ordinary method+path row: this is not a single-endpoint vendor.

A quote deliberately does not get a third census entry: for the split this census exists to express — answering someone versus speaking under the org's own name — a quote is an original post, so it resolves to the x.tweets.create side and the partition stays two-sided and total.

The discriminator is the pack's own, not a vocabulary minted for the census: src/semantics/posts.ts branches on those exact fields to decide what it writes (conversation_id, in_reply_to_user_id, referenced_tweets), and x-connector.ts sends the same fields back to the real vendor on a push. src/x-spec-census.test.ts holds those three to each other, so a rename in one goes red.

Relationship to @volter/twin-xidentity

xidentity owns X's OAuth — the OAuth 2.0 authorize screen and authorization-code + PKCE round trip, the OAuth 1.0a three legs (oauth/request_token, the oauth/authorize / oauth/authenticate screen, oauth/access_token) and GET /2/users/me. This pack owns the posting surface and consumes the credentials those flows mint. It never models OAuth: duplicating it here would be two accounts of one vendor's auth.

One credential store, read across the seam. X has one store of OAuth 1.0a access tokens, and it is xidentity's. An access token a person approved there must also sign a post here, so src/x-oauth1.ts reads xidentity's oauth1_token and oauth1_app rows through the kernel's owner read (projectOwnerResources, a pair scripts/architecture.test.ts declares in OWNER_READS) (read-only; the row shapes are the contract xidentity-oauth1.ts states) and verifies the signature with a transcription of its RFC 5849 verifier. The token acts for its user_id, which is the X account in this pack's state: a World that posts as a person seeds that account (id and handle) here, as it does for a /_twin/tokens bearer. The World's own App — the X_API_KEY / X_API_SECRET the World sets (Postiz's names; TWITTER_API_KEY / TWITTER_API_SECRET also) — is read with worldEnvValue on both sides. src/x-oauth1.integration.test.ts runs both packs on one root (xidentity from its own bin) and drives Postiz's path with twitter-api-v2: the three legs, v2.me(), v2.uploadMedia, a post, and Postiz's own signOAuth1.

The two agree about X rather than sharing code: scripts/architecture.test.ts (A3) forbids a cross-vendor pack import, so src/x-problems.ts and src/x-scopes.ts transcribe xidentity's problem envelopes and scope catalog, and say so at the seam; the problem types are the vendored document's (resource-not-found at https://api.x.com/2/problems/, as its ResourceNotFoundProblem names it), and the scopes an operation needs are its own security, read from the spec.

Both packs claim api.x.com in the injector, path-scoped and disjoint: xidentity claims /2/oauth2/{token,revoke}, /2/users/me, the OAuth 1.0a legs and its consent screen's assets; this claims every other /2/ path and the rest of /_twin/, by a lookahead that leaves xidentity's out, so no request is claimable by both. An operation of X's spec this pack does not model answers X's 404 problem here (the gap); a path outside the X API v2 (the v1.1 API) is unclaimed and refused by the injector.

x.com's public pages

An app that posts links the person to the post where it was published (Postiz: https://twitter.com/<handle>/status/<id>). Inside a World that link opens here: this pack claims x.com and twitter.com for exactly two anchored path shapes and serves them from its store (src/screens/x-web.ts), with no sign-in, as X shows a public page to anyone:

  • /<handle>/status/<id> — the post's page: the author's name, @handle and verified mark, the text with its mentions and links, its images or video, a quoted post, and the full time (drawn in UTC — the page has no viewer's zone). Under another handle, or the author's in another case, it is a 307 to the author's own URL, as x.com answers; a deleted post or an id no post has is a 404 with X's "this page doesn't exist" words; a protected account's post shows X's protected notice.
  • /<handle> — the profile (name, handle, bio, location, website, joined date) over its posts, newest first, each linking to its page; replies stay off it (X's Posts tab). The handle is case-insensitive; a handle no account has is a 404 ("This account doesn't exist").
  • An account without a photo is drawn with X's default avatar — the grey silhouette at the default profile_image_url the API answers for it, redrawn inline. A photo an account set is not drawn yet (todo x.web.profile_photo): it keeps a lettered placeholder.
  • abs.twimg.com/sticky/default_profile_images/default_profile_{normal,bigger,200x200,400x400}.png — that default avatar as an app fetches it (Postiz stores it at channel connect): image/png at X's sizes (48, 73, 200 and 400 px square), a two-colour PNG of X's #657786 silhouette on #ccd6dd drawn by the twin, identical bytes every time. Only those four paths on the host are claimed; X's _mini and bare default_profile.png are not (todo x.web.default_profile_other_variants).
  • A trailing slash is a 307 to the path without it. twitter.com answers with a 301 to x.com, www.x.com and www.twitter.com with a 301 to twitter.com, mobile.twitter.com with a 302 to twitter.com and mobile.x.com with a 302 to x.com.

Those status codes are x.com's own, observed logged out on 2026-09-28 (curl -A 'Mozilla/5.0'; the requests are in src/screens/x-web.ts). X's top-level routes (home, grok, premium, …) are never read as a handle.

Images and video on those pages load from pbs.twimg.com and video.twimg.com — X's own media hosts — which this pack also claims, for exactly the paths it serves from its blob store. x.com is shared with xidentity (its OAuth consent screen at /i/oauth2/authorize and its /_twin/ doors); the handle rule never admits i or _twin, so the two claims are disjoint (scripts/vendor-hosts.test.ts). Replies under a post and x.com's logged-out chrome are manifest todos (x.web.*).

The mirror — x.com's view of the twin

A person doing X's core job opens a browser, so this pack ships a mirror: x.com's own look over the twin's state, for judging a post as it will appear.

bun packages/twin/x/src/cli.ts mirror --root /tmp/world --port 4700   # or: volter twin x mirror --root <state> --port <p>
  • Sign in is x.com's two-step login: the account's @handle, then — in place of a password — the bearer the twin issued for it through /_twin/tokens. The mirror checks the token is that account's with X's own rule (the home timeline is served only to its own account). Handle and token live in the tab's sessionStorage; the mirror keeps nothing else.
  • Screens: the profile (header, Posts / Replies), a single post's page with its reply box and the replies the account can read, Notifications → Mentions, and Home with the compose box. A post's media is x.com's rounded block under its text: one image at its own aspect, two to four in a grid, or a video player — its own first frame, the play button and the duration badge until it plays, then the player's controls. The composer posts text only.
  • Reads are the routes above — GET /2/users/by/username/:username, /2/users/:id/tweets, /2/users/:id/mentions, /2/users/:id/timelines/reverse_chronological, /2/tweets/:id, each expanding attachments.media_keys, and the media URLs they return — on the same origin (src/x-mirror-ui.ts mounts the pack's own fetch adapter). Writes are POST /2/tweets: a post, or a reply with reply.in_reply_to_tweet_id.
  • What it does not draw: engagement counts and the profile's Following / Followers numbers — the twin models no public_metrics, so the slots are empty rather than zero. The avatar is a lettered placeholder; the twin serves no profile images.
  • Not yet readable inside a served World. volter world serve has no mirror mount (only apps/cloud mounts /<org>/<world>/<vendor>/mirror/), and on a World's wire an Authorization: Bearer is read as the World's own credential ahead of the browser's session cookie, so the mirror's X bearer is refused there as token required.

"Drafts in an outbox" is deliberately not here. A draft sitting until someone approves it is an obligation on the posting action — org config — and building an outbox into a vendor pack would put behavior policy one layer further from where it belongs than putting it in platform code.

Evidence boundary

No X API call is made anywhere in this pack, by any test, ever. X's rate limits are punitive and this estate has been bitten. Every route and body shape is X's own OpenAPI document (fetched once, unauthenticated, for vendoring: spec/SOURCE.md), corrected where it lags the wire by patches whose evidence is checked (spec/patches.json, spec/sources.json), and held against the pinned [email protected] client Postiz uses (src/x-sdk.integration.test.ts); the census's curated slice keeps its own provenance (test-fixtures/x-openapi-operations.SOURCE.md). The rate-budget ceiling is explicitly not a transcription: X's published per-endpoint figures in this repo cover GET /2/users/me, not the posting endpoints, so the declaration is deliberately conservative and files x.rate_limit.published_figures as a pinning todo. Wording for refusals X has never shown us is filed the same way (x.errors.*_wording) rather than dressed up as vendor prose.