twitter-api-safe
twitter-api-safe is a TypeScript monorepo for sending Twitter/X Web App API requests through a logged-in browser.
The workspace contains six main entry points:
twitter-api-safe-inject: the canonical MAIN-worldsetup.jsasset shared by Playwright and browser extensions.twitter-api-safe-request: an npm package you can use directly from your own Playwright code.twitter-api-safe-wxt: a WXT adapter for browser extensions.twitter-api-safe-relay: an HTTP relay server that wrapstwitter-api-safe-request.twitter-api-safe-mcp: an MCP server that runs the relay and serves MCP in a single process.twitter-api-safe-dump: records the request catalog (requests.ndjson) that the relay serves atGET /requests.ndjson.
flowchart LR
app["Your Node.js app"]
extension["WXT browser extension"]
curl["HTTP client"]
agent["AI agent (MCP client)"]
server["twitter-api-safe-relay"]
mcp["twitter-api-safe-mcp"]
package["twitter-api-safe-request"]
wxt["twitter-api-safe-wxt"]
xclient["X Web App<br/>internal API client"]
app -->|"call package"|package
extension -->|"call package"|wxt
curl -->|"HTTP request"|server
agent -->|"MCP"|mcp
server -->|"call package"|package
mcp -->|"call package"|package
package -->|"browser bridge"|xclient
wxt -->|"MAIN world bridge"|xclient
Use the package directly
Use twitter-api-safe-request when you already have a Playwright browser/page and do not need an HTTP server.
pnpm add twitter-api-safe-request playwright
If needed:
pnpm exec playwright install
import { chromium } from "playwright";
import { createTwitterBrowser } from "twitter-api-safe-request";
const context = await chromium.launchPersistentContext("./user_data/account1", {
headless: false,
});
const page = await context.newPage();
const client = createTwitterBrowser(page);
await client.inject();
await client.goto("https://x.com/home");
const result = await client.dispatch({
method: "GET",
path: "/2/users/me",
params: {},
});
Package README: packages/request/README.md
npm package page: twitter-api-safe-request
Use from a WXT browser extension
Add the module to wxt.config.ts:
export default defineConfig({
modules: ["twitter-api-safe-wxt/module"],
});
Create the browser client from a document_start content script:
import { createTwitterBrowser, TWITTER_MATCHES } from "twitter-api-safe-wxt";
export default defineContentScript({
matches: [...TWITTER_MATCHES],
runAt: "document_start",
allFrames: false,
async main(ctx) {
const client = createTwitterBrowser(ctx);
await client.inject();
await client.waitStartup();
const result = await client.dispatch({
method: "GET",
path: "/2/example",
params: {},
});
console.log(result);
},
});
Package README: packages/wxt/README.md
npm package page: twitter-api-safe-wxt
Runnable example: examples/basic
Use the HTTP relay
Use twitter-api-safe-relay when another process should call the X Web App API over HTTP.
Once the relay is running, replace the X.com origin with the relay origin and keep the path/query/body shape:
You do not need to provide Cookie, CSRF, or x-client-transaction-id yourself; the relay adds those headers automatically.
https://x.com/i/api/graphql/{queryId}/{operationName}?...
http://localhost:3000/i/api/graphql/{queryId}/{operationName}?...
For example:
curl 'http://localhost:3000/i/api/graphql/gKia-nBM9kwuDEfSDeWMfQ/HomeTimeline'
Server README: packages/server/README.md
npm package page: twitter-api-safe-relay
Use from AI agents (MCP)
twitter-api-safe-mcp runs the full relay server (HTTP API + dashboard) and serves MCP in a single process. It exposes list_profiles and twitter_api_request tools.
claude mcp add twitter-api-safe -- pnpx twitter-api-safe-mcp ./settings.json
Package README: packages/mcp/README.md
npm package page: twitter-api-safe-mcp
Use from AI agents (Agent Skills)
twitter_api_safe_relay_skills provides Agent Skills for Claude Code and other agents, letting an AI turn natural-language instructions into relay-server GraphQL / v1.1 calls and summarize the results.
npx skills add fa0311/twitter_api_safe_relay_skills
Start the relay server, then point the skill at it with the TWITTER_RELAY_BASE_URL environment variable. The agent only sees the operation catalog — it never touches cookies, CSRF tokens, or other credentials directly.
Request catalog
The relay serves GET /requests.ndjson: one request object per line, the latest request the X web app sent for every operation it uses. The MCP server and the skill pick an operation from it and only change the variables; queryId, features and fieldToggles stay as recorded. The file lives in packages/server/requests.ndjson and is recorded, not written by hand:
pnpm dump # twitter-api-safe-dump --user-data-dir ./user_data --out packages/server/requests.ndjson
It browses a fixed list of x.com pages with the logged-in profile and keeps the latest request of every operation, with only the content-type header. Package README: packages/dump/README.md
Configuration
Configure the relay in the workspace-level settings.json.
{
"port": 3000,
"logger": {
"level": "info"
},
"profiles": [
{
"name": "account1",
"browser": {
"type": "launch",
"userDataDir": "./../../user_data/account1",
"headless": false
}
}
]
}
Each profile uses either:
launch: Playwright opens a persistent browser profile. The browser starts on the first request; sign in to X/Twitter when it opens.cdp: the relay connects to an already-running Chromium browser over the Chrome DevTools Protocol.
Docker
Images are published to ghcr.io/fa0311/twitter_api_safe_relay. Every image has two tags: latest- and , where is the npm package version (for init-profile, a build hash instead).
| Tag () | Contents |
| --------------- | ------------------------------------------------------------------------------------- |
| relay | Relay server with bundled Chromium, for launch profiles. |
| relay-slim | Relay server without a browser, for cdp profiles connecting to an external browser. |
| relay-firefox | Relay server with bundled Firefox. |
| relay-webkit | Relay server with bundled WebKit. |
| mcp | MCP server (runs the relay + MCP) with bundled Chromium. |
| mcp-slim | MCP server without a browser, for cdp profiles connecting to an external browser. |
| mcp-firefox | MCP server with bundled Firefox. |
| mcp-webkit | MCP server with bundled WebKit. |
| init-profile | Helper that resets lock files and permissions on the browser profile volume. |
The compose example in docker/ runs the relay with the debug dashboard enabled, using relay-slim connected over CDP to a kasmweb Chromium container, with init-profile preparing the shared profile volume.
docker compose -f docker/docker-compose.yml up
Open the browser UI at http://localhost:6901, sign in to X/Twitter, then call the relay or dashboard on http://localhost:6900.
Local development
pnpm install
pnpm build
pnpm dev:relay
This starts the relay server and the Vite UI.
If you use a launch profile and do not already have the browser installed:
pnpm exec playwright install chromium
Tests
pnpm --filter twitter-api-safe-relay-dashboard test
pnpm --filter twitter-api-safe-request test
pnpm --filter twitter-api-safe-relay test
The request and relay suites drive a real browser against x.com. A fresh profile is redirected to the login onboarding, so give them a logged-in session and, if your network is challenged by Cloudflare (GitHub-hosted runners are), an HTTP proxy:
X_COOKIE_FILE=cookie.json \ # browser extension cookie export (same format as fa0311/twitter-secrets)
X_PROXY_URL=http://user:pass@host:1080 \
pnpm --filter twitter-api-safe-request test