Events
Every event the feed socket sends, X and socials, with its payload.
Every event is an event name plus one JSON payload: listen by name with socket.on(name, handler). Object types are in Data Types. Non-X posts all come as external_message.
How a post arrives
A new post is sent once as tweet, then again as tweet_update each time a fuller version comes in. J7 ingests X from more than one provider, and each provider sends a fast version first and richer versions later. Whichever copy reaches J7 first becomes the tweet; every later copy of the same id becomes a tweet_update.
tweet → first copy: text, author, often no media yet
tweet_update → media / quoted post / card resolved
tweet_update → long-form body, article, translation hydratedKeep one map keyed on id and merge. A later update can carry less than an earlier one (a fast path that skipped media), so never overwrite a non-empty field with an empty one.
initialTweets
The backlog, sent once right after user_connected. Up to 100 items, newest first, with your hidden accounts removed. It mixes X posts with social posts and webhook cards: tell them apart by source === 'external' and isWebhook.
Only sent when at least 10 items remain, so don't wait on it. Replace your list with it.
type InitialTweetsEvent = (Tweet | SocialPost | WebhookCard)[];tweet [1]
The first copy of a new post from a watched X account. Drop it if you already have its id: custom accounts can deliver it twice (fast and full versions).
type TweetEvent = Tweet;tweet_update [2]
A newer, fuller copy of a post that was already sent. Sent every time media, quoted posts, articles, translations or metrics resolve, often several times per post.
If you don't have the id yet, treat it as a new tweet.
type TweetUpdateEvent = Tweet;quoted_tweet
The quoted post on its own, sent just before the tweet / tweet_update that quotes it. The parent already contains it as quotedTweet, so this can be ignored.
type QuotedTweetEvent = NestedTweet;tweet_deleted
Sent when a watched account deletes a post.
interface TweetDeletedEvent {
id: string; // The deleted post's id
deletedTweet: Tweet | null; // J7's cached copy if it still had one. Always null for custom accounts
tweet: RawTweet; // Upstream copy of the post, present even when `deletedTweet` is null
isCustomAccount?: true;
}following_update
Sent when a watched account follows someone. On the main feed, follow / unfollow / profile events are deduplicated server-side within 30 s.
interface FollowingUpdateEvent {
id: string; // `follow_<user>_<following>_<ms>`. On the main feed a `follow_scan` with this id follows
user: TwitterUser; // The watched account
following: TwitterUser; // The account they followed
others: TwitterUser[]; // Other watched accounts that recently followed the same target
provider?: string; // Main feed only
isCustomAccount?: true;
}unfollowing_update
Sent when a watched account unfollows someone.
interface UnfollowingUpdateEvent {
user: TwitterUser; // The watched account
unfollowing: TwitterUser; // The account they unfollowed
provider: string;
}affiliated_update
Sent when a watched business account adds an affiliate.
interface AffiliatedUpdateEvent {
id?: string; // Main feed only. A `follow_scan` for `affiliate` follows
user: TwitterUser; // The organisation
affiliate: TwitterUser; // The account it added
}unaffiliated_update
Sent when a watched business account removes an affiliate.
interface UnaffiliatedUpdateEvent {
user: TwitterUser; // The organisation
affiliate: TwitterUser; // The account it removed
}profile_update
Sent when a watched account changes its name, bio, avatar, banner, location, website or privacy.
interface ProfileUpdateEvent {
user: TwitterUser; // The profile after the change
before: TwitterUser; // The profile before the change
// Main feed only. On custom accounts, diff `user` against `before` yourself:
modifications?: (
| 'profile.name'
| 'profile.description'
| 'profile.avatar'
| 'profile.banner'
| 'profile.location'
| 'profile.url'
)[];
privated?: boolean; // The account went private
unprivated?: boolean; // The account went public
}profile_affiliation_update
Sent when a watched account's own affiliation changes.
interface ProfileAffiliationUpdateEvent {
user: TwitterUser;
affiliation: {
from: TwitterUser | null; // Previous organisation (if any)
to: TwitterUser | null; // New organisation (if any)
};
}profile_pinned_update
Sent when a watched account pins a post.
interface ProfilePinnedUpdateEvent {
user: TwitterUser;
pinned: Tweet[]; // The newly pinned post(s)
}profile_unpinned_update
Sent when a watched account unpins a post.
interface ProfileUnpinnedUpdateEvent {
user: TwitterUser;
unpinned: Tweet[] | RawTweet[]; // RawTweet on custom accounts
}profile_suspended_update
Sent when a watched account is suspended.
interface ProfileSuspendedUpdateEvent {
user: TwitterUser;
type: 'suspended';
}profile_deactivated_update
Sent when a watched account is deactivated.
interface ProfileDeactivatedUpdateEvent {
user: TwitterUser;
type: 'deactivated';
}token_meta
Token info for a contract address found in an item you received (a tweet, social post or account event). Up to 5 per item. It can arrive before the item, so buffer it for a few seconds.
interface TokenMetaEvent {
id: string | null; // The parent item's id
mint: string; // Solana mint, or EVM address (lowercased)
tokenMeta: {
mint: string;
symbol: string;
name: string;
image: string;
marketCapUsd: number;
marketCapSol: number | null; // null for EVM tokens
market: string; // e.g. `pumpfun`
at: number; // Epoch ms the data was fetched
chain?: string; // EVM only
liquidityUsd?: number; // EVM only
isHoneypot?: boolean; // EVM only
};
}follow_scan
The latest contract address posted by the account in a following_update / affiliated_update, matched on id.
interface FollowScanEvent {
id: string; // Same id as the following_update / affiliated_update
handle: string; // The followed / affiliated account
scan: {
cached: boolean;
contracts:
| {
ok: true;
indexing: boolean; // true while the account is still being indexed (rows is empty)
total: number;
deleted: number;
undeleted: number;
rows: {
ca: string;
chain: string;
tweetId: string;
twitterHandle: string;
tweetTime: string;
isDeleted: boolean;
content: string;
}[]; // Latest CA only
}
| { ok: false; error: string };
};
}external_message
Every non-X post: Truth Social, Instagram, TikTok, YouTube, Binance Square, J7 webhook cards and their media patches. Social posts are global (everyone gets every one, so filter by author.handle yourself) and are also replayed in initialTweets. All platform posts extend SocialPost.
type ExternalMessageEvent = SocialPost | WebhookCard | SocialPatch;
socket.on('external_message', (m) => {
switch (m.type) {
case 'TIKTOK_VIDEO_UPDATE':
case 'YOUTUBE_IMAGE_UPDATE':
case 'YOUTUBE_DELETION':
return patch(m); // Updates a post you already have, matched on id
}
if (m.isWebhook) return webhook(m);
if (m.isTruthSocial) return truth(m);
if (m.isInstagram) return instagram(m);
if (m.isTikTok) return tiktok(m);
if (m.isYouTube) return youtube(m);
if (m.isBinanceSquare) return binanceSquare(m);
});Flags that don't apply are false or missing, so test for truthy.
Truth Social
interface TruthSocialPost extends SocialPost {
platform: 'TRUTH SOCIAL';
isTruthSocial: true;
truthSocialUrl: string;
truthSocialPostType?: 'RETWEET' | 'QUOTE';
card: { title: string; description: string; url: string; image: string | null } | null;
// ReTruth (isRetweet: true). Top-level `text` and `media` are empty; the original is in `quotedTweet`
quotedTweet: TruthSocialRef | null;
originalAuthor?: SocialPost['author'];
retweetedUser?: SocialPost['author'];
originalTweetText?: string;
originalMedia?: SocialPost['media'];
originalTweetUrl?: string;
// Quote (isQuote: true)
quotedUser?: SocialPost['author'];
}
interface TruthSocialRef {
id: string;
text: string;
author: SocialPost['author'];
media: SocialPost['media'];
truthSocialUrl: string;
card?: TruthSocialPost['card']; // ReTruths
nestedQuote?: TruthSocialRef | null; // Quotes: a quote inside the quote
}Video posters are added to media.images.
interface InstagramPost extends SocialPost {
platform: 'INSTAGRAM';
isInstagram: true;
instagramUrl: string; // Stories look like /stories/<user>/<id>/
instagramPostType: 'POST' | 'REEL' | 'STORY'; // Defaults to POST
hashtags: string[];
mentions: string[];
mediaCount: number;
}TikTok
The video URL is often not ready yet, so media.videos starts empty and a TIKTOK_VIDEO_UPDATE patch fills it in.
interface TikTokPost extends SocialPost {
platform: 'TIKTOK';
isTikTok: true;
tiktokUrl: string;
tiktokEventType: 'UPLOAD' | 'REPOST' | 'LIVE_STARTED';
tiktokKind: 'video' | 'photo' | 'carousel' | 'live';
tiktokVideoId: string | null;
tiktokRoomId: string | null; // Set for LIVE_STARTED
hashtags: string[];
mentions: string[];
mediaCount: number;
duration: number | null; // Seconds
music: { title: string; author: string; original: boolean } | null;
originalAuthor: SocialPost['author'] | null; // REPOST (isRetweet: true)
}LIVE_STARTED posts have tiktokKind: 'live' and text starting with 🔴.
YouTube
interface YouTubePost extends SocialPost {
platform: 'YOUTUBE';
isYouTube: true;
id: string; // `yt-<videoId>`
youtubeUrl: string;
youtubeVideoId: string;
youtubeChannelId: string; // `UC…`. Identify channels by this: handles can change
youtubeChannelUrl: string;
youtubeSubtype: 'VIDEO' | 'SHORT';
youtubeTitle: string;
youtubeDuration: string; // e.g. `12:34`
youtubeViewCount: number;
youtubeDeleted: boolean;
text: string; // Title, a blank line, then up to 400 characters of the description
// author.handle is the @handle when known, otherwise the `UC…` channel id
}Binance Square
Newly pinned posts are sent as regular posts.
interface BinanceSquarePost extends SocialPost {
platform: 'BINANCE SQUARE';
isBinanceSquare: true;
id: string; // `bsq-<postId>`
binanceSquareUrl: string;
contentType: string;
text: string; // Title, a blank line, then the body
poll: {
question: string;
ends_at: string;
closed: boolean;
choices: { label: string; count: number; image: string | null }[];
} | null;
metrics: {
reply_count: number;
like_count: number;
comment_count: number;
share_count: number;
view_count: number;
};
translation: { from: string; to: string; text: string } | null;
tendency: string;
coinPairs: string[]; // e.g. `BNBUSDT`
hashtags: string[];
mentions: string[];
quotedTweet: BinanceSquareRef | null;
replyTo: BinanceSquareRef | null;
originalAuthor: SocialPost['author'] | null; // Quotes: the quoted author
}
interface BinanceSquareRef {
id: string;
text: string;
author: SocialPost['author'];
media: SocialPost['media'];
poll: BinanceSquarePost['poll'];
translation: BinanceSquarePost['translation'];
coinPairs: string[];
binanceSquareUrl: string;
}Webhook cards
J7's own announcement cards, shaped like Discord embeds. Current accounts (webhook.handle): stonkpairs (NEW STONKFUN PAIR), ponspairs (NEW PONS PAIR) and pumppairs (NEW PUMP PAIR). Unlike social posts, webhook text is not HTML-encoded.
interface WebhookCard {
id: string; // e.g. `wh-pumppairs:AAPLx`
type: 'WEBHOOK';
source: 'webhook';
isWebhook: true;
createdAt: number; // Epoch ms
timestamp: number; // Epoch ms
author: {
name: string; // Display name
handle: string; // `webhook:<webhook.handle>`
avatar: string;
verified: false;
};
text: string;
webhook: {
handle: string; // Stable account key
displayName: string;
username: string; // Same as displayName
avatarUrl: string;
url: string;
badge: string;
content: string;
embeds: WebhookEmbed[]; // Up to 10
};
}
interface WebhookEmbed {
author: { name: string; url: string; iconUrl: string };
title: string;
titleSize: string;
url: string;
description: string;
address: string; // Solana or EVM contract address, or ""
color: string; // Hex, e.g. `#4ade80`
fields: { name: string; value: string; inline: boolean }[]; // Up to 25
image: string;
thumbnail: string;
thumbnailPosition: string;
thumbnailSize: string;
thumbnailShape: string;
footer: { text: string; iconUrl: string };
timestamp: number;
}Patches
These update a post you already have instead of adding a new one. Match them on id.
// Add `{ url: videoUrl, poster }` to the TikTok post's media.videos
interface TikTokVideoUpdate {
type: 'TIKTOK_VIDEO_UPDATE';
id: string;
tiktokUrl: string;
videoUrl: string;
poster: string;
}
// Replace the YouTube post's thumbnail
interface YouTubeImageUpdate {
type: 'YOUTUBE_IMAGE_UPDATE';
id: string;
videoId: string;
imageUrl: string;
}
// The video was deleted: set `youtubeDeleted: true` on your copy
interface YouTubeDeletion {
type: 'YOUTUBE_DELETION';
id: string;
videoId: string;
youtubeUrl: string;
}
type SocialPatch = TikTokVideoUpdate | YouTubeImageUpdate | YouTubeDeletion;charity_event
The pump.fun charity registry, relayed as-is from upstream as it changes. Upstream doesn't guarantee every field, so read charity first and fall back to summary.
interface CharityEvent {
type: 'new_charity' | 'updated_charity' | 'deleted_charity';
time?: string; // ISO time
charity?: {
id: string;
name: string;
slug: string;
isVerified: boolean;
logo?: string;
city?: string;
state?: string;
country?: string;
tax_id?: string;
website?: string;
};
summary?: { id: string; name: string; slug: string; verified: boolean };
}hidden_accounts_updated
Your full hidden X list, sent after you remove (hide) or re-add (unhide) a main-feed account. Replace your copy, don't merge.
interface HiddenAccountsUpdatedEvent {
hidden: string[]; // Lowercased handles
}status
Whether J7's upstream X feed is connected. This is not your own socket's state.
interface StatusEvent {
connected: boolean;
}auth_error
The JWT sent in user_connected was rejected. See Errors & Limits.
interface AuthErrorEvent {
error: 'Invalid session' | 'Too many connections';
}pong
Reply to your ping. No payload.