xnotif
v0.2.1
Published
Receive Twitter/X push notifications programmatically via Mozilla Autopush
Maintainers
Readme
xnotif
Receive Twitter/X notifications in real-time. No API key, no scraping — just Web Push.
import { createClient } from "xnotif";
const client = createClient({
cookies: { auth_token: "...", ct0: "..." },
});
client.on("notification", (n) => {
console.log(`${n.title}: ${n.body}`);
});
await client.start();Install
npm install xnotifRequires Node.js >= 22.0.0
Why xnotif
- Cookie exposure can be minimized - Cookies are mainly used for one registration call to
POST /1.1/notifications/settings/login.json. After registration, notifications are received through Mozilla Autopush WebSocket, so you are not continuously calling internal Twitter endpoints with cookies. - Lower ban-risk profile than polling/scraping - xnotif avoids headless-browser automation and high-frequency private API polling. The runtime traffic pattern is mostly one registration plus push-stream consumption.
- Avoid unnecessary re-registration - If you persist
ClientStatefrom theconnectedevent, restart withstate, and the endpoint is unchanged, xnotif skips the registration call. - Simple operations - No API key provisioning, no webhook server, and no request-signing stack. A single Node.js process can receive and process notifications.
Notification Payload
Each notification event delivers a TwitterNotification object:
{
"title": "@jack",
"body": "just setting up my twttr",
"icon": "https://pbs.twimg.com/profile_images/...",
"timestamp": 1142974214000,
"tag": "mention_12345",
"data": {
"type": "mention",
"uri": "https://x.com/i/web/status/20",
"title": "@jack",
"body": "just setting up my twttr",
"tag": "mention_12345",
"lang": "en",
"scribe_target": "mention",
"impression_id": "abc123",
},
}Top-level fields:
| Field | Type | Description |
| ----------- | --------- | ------------------------------------------------- |
| title | string | Who triggered the notification |
| body | string | Human-readable description |
| icon | string? | Profile image URL |
| timestamp | number? | Unix epoch in milliseconds |
| tag | string? | Deduplication tag |
| data | object? | Structured metadata (see data.type for routing) |
Getting Cookies
- Log in to x.com
- DevTools → Application → Cookies
- Copy
auth_tokenandct0
State Persistence
Save the ClientState from the connected event to skip key generation on restart:
import { createClient, type ClientState } from "xnotif";
let state: ClientState | undefined = loadFromDisk(); // your persistence
const client = createClient({ cookies: { auth_token: "...", ct0: "..." }, state });
client.on("connected", (s) => saveToDisk(s));
await client.start();API
createClient(options)
| Option | Type | Required | Description |
| --------- | ------------------------------------------------ | -------- | --------------------------------- |
| cookies | { auth_token: string; ct0: string } | Yes | Session cookies |
| state | ClientState | No | Restore previous state |
| filter | (notification: TwitterNotification) => boolean | No | Predicate to filter notifications |
Filtering Notifications
Pass a filter function to receive only the notifications you care about:
const client = createClient({
cookies: { auth_token: "...", ct0: "..." },
filter: (n) => n.data?.type === "tweet",
});The predicate receives the decrypted TwitterNotification object. Return true to emit the notification, false to discard it silently. If the filter throws an exception, the notification is discarded and an error event is emitted.
Events
| Event | Payload | Description |
| -------------- | --------------------- | ------------------------------ |
| notification | TwitterNotification | Decrypted notification |
| connected | ClientState | Connected — persist this state |
| error | Error | Error (connection continues) |
| disconnected | — | WebSocket closed |
| reconnecting | number | Reconnecting in N ms |
Methods
client.start()— Connect and begin receiving notificationsclient.stop()— Disconnect
Low-level Exports
Decryptor— AESGCM Web Push decryption (ECDH + HKDF + AES-128-GCM)AutopushClient— Mozilla Autopush WebSocket client
How It Works
sequenceDiagram
participant App as xnotif
participant Autopush as Mozilla Autopush<br/>wss://push.services.mozilla.com
participant Twitter as Twitter/X
App->>App: Generate ECDH P-256 key pair + 16-byte auth secret
App->>Autopush: WebSocket connect (subprotocol: push-notification)
Autopush-->>App: hello ACK (uaid assigned)
App->>Autopush: Register channel with VAPID key
Autopush-->>App: Push Endpoint URL
App->>Twitter: POST /1.1/notifications/settings/login.json<br/>{ token: endpoint, encryption_key1: p256dh, encryption_key2: auth }
Twitter-->>App: 200 OK
loop Real-time notifications
Twitter->>Autopush: Web Push (AESGCM encrypted payload)
Autopush->>App: WebSocket message
App->>App: ECDH shared secret (256-bit)<br/>→ HKDF-SHA256 (IKM, CEK, nonce)<br/>→ AES-128-GCM decrypt<br/>→ Strip 2-byte padding
App-->>App: Emit "notification" event
end- Key generation — Generate an ECDH P-256 key pair and a 16-byte auth secret via
crypto.subtle(skipped when restoring from savedstate) - Autopush connection — Open a WebSocket to
wss://push.services.mozilla.comwith thepush-notificationsubprotocol, send ahellohandshake, then register a channel to obtain a Push Endpoint URL - Twitter registration — POST the Push Endpoint, base64url-encoded public key, and auth secret to Twitter's
/1.1/notifications/settings/login.json, authenticated with your session cookies (auth_token/ct0) - Receive & decrypt — When Twitter pushes an AESGCM-encrypted payload through Autopush, derive a shared secret via ECDH, expand it with HKDF-SHA256 into a 16-byte CEK and 12-byte nonce, then decrypt with AES-128-GCM
- Emit — Parse the decrypted JSON into a
TwitterNotificationand fire it as anotificationevent
License
MIT
