j7tracker

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 hydrated

Keep 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.

Instagram

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.

On this page