B2B Realtime API

WebSocket-based realtime stream of tweets, deleted-tweet alerts, follow events, profile changes, and new follower detection for the handles you monitor. Median tweet detection ~200 ms on paid plans (live at /speed/). TweetCatcher-compatible opcode framing.

OpenAPI spec · Postman collection

Quickstart

  1. Subscribe via @B2B_Xanguard_bot on Telegram. Pay in SOL.
  2. The bot's confirmation message contains your raw API key (visible once) — save it now.
  3. Add handles via POST /v1/dt/targets or the bot's /add command.
  4. Connect to wss://api.xanguard.tech/v1/dt/realtime/ws and follow the opcode flow below.

Opcode protocol

Frames are JSON objects with an op integer field. Most carry a d data field; events carry their type in d.event (there is no top-level t field).

CodeNameDirectionPurpose
10HELLOserver → clientFirst frame after upgrade. d.heartbeat_interval is ms.
2LOGINclient → serverReply to HELLO with {"op":2,"d":"YOUR_API_KEY"} within 15s.
4READYserver → clientAuth succeeded. Streaming begins.
0EVENTserver → clientCarries d; the event type is in d.event. See event types below.
1HEARTBEATclient → serverSend every heartbeat_interval ms.
11HEARTBEAT_ACKserver → clientServer's response to your heartbeat.
3DISCONNECTserver → clientConnection closes after this frame. d is the reason string.

Limits: 5 concurrent connections per API key. Stale events (>30s old) are dropped. Heartbeat ACK timeout is 90s.

Late delivery and tweet replay (opt-in)

By default the stream is real-time only: a tweet older than 30 seconds is dropped. If you would rather receive a tweet late than not at all, ask support to enable late delivery on your key. It changes two things for twitter.post.new:

Always deduplicate on event_id. Late and replayed frames use the same event_id as the live frame (evt_<tweet id>), and the same tweet can reach you more than once (a replay after a reconnect, or a late copy after a restart on our side). Keep the first copy and ignore repeats.

Replayed tweets carry the core fields only: no twitter.post.update follows them, data.type is not corrected afterwards (a reply we could not identify as one stays "post"), in_reply_to and quoted_tweet details may be null, entities and metrics are null, and a tweet deleted since detection can still be replayed. All three fields are additive and appear only on those frames; on-time frames are unchanged. Your login filters (types, onlyCA, onlyTicker, categories) apply to late and replayed tweets too.

Event types

twitter.post.new

First delivery of a detected tweet, reply, quote, or retweet from a tracked handle. May carry partial metadata. Store it and replace with twitter.post.update if that arrives for the same data.id. Tweet types in data.type: "post", "reply", "quote", "repost".

{
  "op": 0,
  "d": {
    "event": "twitter.post.new",
    "event_id": "evt_1234567890123456789",
    "task_info": {"handle": "example_dev"},
    "data": {
      "id": "1234567890123456789",
      "created_at": 1712000000000,
      "type": "post",
      "text": "Hello world",
      "media": [{"type": "photo", "url": "https://pbs.twimg.com/media/..."}],
      "mentions": [{"screen_name": "example_founder"}],
      "author": {
        "id": "1234567890",
        "handle": "example_dev",
        "name": "Example Dev",
        "avatar": "https://pbs.twimg.com/profile_images/...",
        "description": "Example account used in these docs",
        "verification": {"is_verified": true},
        "stats": {"followers": 12000, "following": 800}
      },
      "in_reply_to": null,
      "quoted_tweet": null,
      "possibly_sensitive": false
    }
  }
}

twitter.post.update

Enriched re-delivery of a previously sent tweet with complete metadata. Shares the same data.id as the original twitter.post.new. Replace any previously stored data for this tweet ID.

{
  "op": 0,
  "d": {
    "event": "twitter.post.update",
    "event_id": "evt_u_1234567890123456789",
    "task_info": {"handle": "example_dev"},
    "data": {
      "id": "1234567890123456789",
      "created_at": 1712000000000,
      "type": "quote",
      "text": "This is huge 👀",
      "media": [],
      "mentions": [],
      "author": {
        "id": "1234567890",
        "handle": "example_dev",
        "name": "Example Dev",
        "avatar": "https://pbs.twimg.com/profile_images/...",
        "description": "Example account used in these docs",
        "verification": {"is_verified": true},
        "stats": {"followers": 12000, "following": 800}
      },
      "in_reply_to": null,
      "quoted_tweet": {
        "id": "9876543210987654321",
        "text": "Original tweet text here",
        "author": {"handle": "example_founder", "name": "Example Founder"}
      },
      "possibly_sensitive": false
    }
  }
}

twitter.following.new

A tracked handle followed someone new. task_info.handle is the tracked account and task_info.id its numeric Twitter id; data is the account it just followed. Unfollows are not a realtime event — fetch added/removed over a trailing window via GET /v1/dt/social/diff/{handle}.

{
  "op": 0,
  "d": {
    "event": "twitter.following.new",
    "event_id": "evt_f_example_dev_1712000000",
    "task_info": {"handle": "example_dev", "id": "1234567890"},
    "data": {
      "id": "1234567890",
      "handle": "example_founder",
      "name": "Example Founder"
    }
  }
}

twitter.profile.update

A tracked handle changed its bio, name, avatar, or pinned tweet. Fields in data.field: "description", "name", "avatar", "pinned_post".

{
  "op": 0,
  "d": {
    "event": "twitter.profile.update",
    "event_id": "evt_p_example_dev_1712000000",
    "task_info": {"handle": "example_dev"},
    "data": {
      "field": "description",
      "prev": "Old bio text",
      "updated": "New bio text"
    }
  }
}

twitter.follower.new

A single new follower for a tracked handle, emitted as it is observed. Requires the followers module. task_info.handle is the monitored account and task_info.id its numeric Twitter id; data is the account that just followed (data.name may be null).

{
  "op": 0,
  "d": {
    "event": "twitter.follower.new",
    "event_id": "evt_fl_example_dev_1771629216692",
    "task_info": { "handle": "example_dev", "id": "1234567890" },
    "data": { "id": "1890000000", "handle": "someacct", "name": "Some Name" }
  }
}

twitter.tweet.deleted

A tweet from a tracked handle was deleted. Requires the realtime module. task_info.handle is the monitored account; data.tweet_id may be null when a deletion is known but the specific tweet isn't.

{
  "op": 0,
  "d": {
    "event": "twitter.tweet.deleted",
    "event_id": "evt_del_example_dev_1771629216692",
    "task_info": { "handle": "example_dev" },
    "data": { "tweet_id": "1899000000000000000" }
  }
}

Code samples

JavaScript (browser / Node)

const ws = new WebSocket('wss://api.xanguard.tech/v1/dt/realtime/ws');
let heartbeat;

ws.onmessage = (raw) => {
  const frame = JSON.parse(raw.data);
  switch (frame.op) {
    case 10: // HELLO
      ws.send(JSON.stringify({ op: 2, d: 'dt_YOUR_API_KEY' })); // LOGIN
      heartbeat = setInterval(
        () => ws.send(JSON.stringify({ op: 1 })),
        frame.d.heartbeat_interval
      );
      break;
    case 4:  console.log('streaming'); break;             // READY
    case 0:  console.log(frame.d.event, frame.d.data); break; // EVENT
    case 11: break;                                       // HEARTBEAT_ACK
    case 3:  console.error('disconnected:', frame.d); break;
  }
};
ws.onclose = () => clearInterval(heartbeat);

Python (asyncio)

import asyncio, json
import websockets

async def stream():
    async with websockets.connect('wss://api.xanguard.tech/v1/dt/realtime/ws') as ws:
        hello = json.loads(await ws.recv())                     # OP_HELLO
        await ws.send(json.dumps({'op': 2, 'd': 'dt_YOUR_API_KEY'}))
        heartbeat_ms = hello['d']['heartbeat_interval']

        async def hb():
            while True:
                await asyncio.sleep(heartbeat_ms / 1000)
                await ws.send(json.dumps({'op': 1}))
        asyncio.create_task(hb())

        async for raw in ws:
            frame = json.loads(raw)
            if frame['op'] == 0:                                # EVENT
                print(frame['d']['event'], frame['d']['data'])

asyncio.run(stream())

REST API (handle management)

MethodPathPurpose
GET/v1/dt/targetsList tracked handles.
POST/v1/dt/targetsAdd handle: {"handle":"example_dev"}
DELETE/v1/dt/targets/{handle}Stop tracking.
GET/v1/dt/statusSubscription tier, expiry, slot usage.

All authenticate via ?api_key=dt_... query parameter. Full schemas in the OpenAPI spec.

Error reference

CodeCauseAction
401Missing/wrong/expired API keyUse /apikey in the bot to regenerate.
403Plan limit (account_limit) exceededUpgrade plan.
429Rate-limit exceeded (REST) or 5-conn cap (WS)Back off; close idle connections.
OP_DISCONNECTWS-level error; d is the reasonReconnect with backoff.

Support

Questions: @0xDeep on Telegram. Bug reports preferred with payment_id + timestamp.