Copyright © 2025 Dankest, LLC
XChain Explorer WebSocket API Reference
The xchain-explorer provides a WebSocket API for real-time streaming of blockchain events. Clients connect to a coin-scoped endpoint and subscribe to channels with optional filters. The server pushes events as they are indexed. No polling required.
Connection
URL Pattern
For the public XChain Platform (mainnet or testnet):
wss://explorer.xchain.io/{COIN}/api/websocket
For a self-hosted or local explorer instance:
ws://host:8080/{COIN}/api/websocket
wss://host:8081/{COIN}/api/websocket
The {COIN} segment determines the chain and network for the connection:
| Coin Prefix | Chain | Network |
|---|---|---|
BTC |
Bitcoin | mainnet |
TBTC |
Bitcoin | testnet |
RBTC |
Bitcoin | regtest |
LTC |
Litecoin | mainnet |
TLTC |
Litecoin | testnet |
RLTC |
Litecoin | regtest |
DOGE |
Dogecoin | mainnet |
TDOGE |
Dogecoin | testnet |
RDOGE |
Dogecoin | regtest |
Example
# Connect with wscat, against the public platform
wscat -c wss://explorer.xchain.io/BTC/api/websocket
# Or against a self-hosted / local explorer instance
wscat -c ws://localhost:8080/BTC/api/websocket
WELCOME Message
Sent automatically on connection. Provides server info, current state, limits, and supported features.
{
"type": "WELCOME",
"chain": "BTC",
"network": "mainnet",
"timestamp": 1743638400000,
"data": {
"version": "1.11.0",
"server_time": 1743638400000,
"latest_block_index": "890122",
"latest_action_index": "45677",
"limits": {
"max_subscriptions": 25,
"max_message_rate": 10,
"max_message_size": 1024,
"max_connections_per_ip": 5
},
"channels": ["blocks", "actions", "mempool", "network", "attestation", "address", "token", "market", "dispenser", "bet_feed"],
"types": ["ORDER", "ORDER_MATCH", "ORDER_EXPIRE", "COINPAY", "..."],
"features": ["snapshot", "once", "fields", "ticks", "batch", "catch_up"]
}
}
Use latest_block_index and latest_action_index to seed your local state for catch-up on reconnect.
Client-to-Server Messages
All client messages are JSON with an action field. An optional id field enables request-response correlation; the server echoes it back in the corresponding response.
subscribe
{
"action": "subscribe",
"id": "sub-1",
"channels": ["blocks", "actions"],
"params": {
"types": ["SEND", "ORDER_MATCH"],
"ticks": ["PEPE", "XCHAIN"],
"fields": ["action_index", "source", "amount"],
"snapshot": true,
"once": true,
"since_action_index": 45677
}
}
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | yes | "subscribe" |
id |
string | no | Echoed back in SUBSCRIBED response |
channels |
string[] | yes | Channels to subscribe to |
params |
object | no | Filter and subscription options |
Params:
| Param | Type | Description |
|---|---|---|
types |
string[] | Only receive events matching these action types. Omit for all. |
statuses |
string[] | Not supported. Do not use. Accepted by the server for backward compatibility but never honored on any channel: no feed populates a per-event status (getActionsSince selects NULL as status), so the filter can never reject anything. It is absent from WELCOME features and from SUBSCRIBED active_filters. See AML #2349. |
ticks |
string[] | Only receive events involving these token tickers (global actions channel). |
fields |
string[] | Only include these keys in the data payload. Envelope fields always included. |
snapshot |
boolean | Send current entity state immediately on subscribe. |
once |
boolean | Auto-unsubscribe after the first matching event. |
since_action_index |
number | Replay missed events since this action_index (for catch-up on reconnect). |
Entity params (required for entity channels):
| Channel | Singular | Batch (plural) |
|---|---|---|
address |
"address": "1abc..." |
"addresses": ["1abc...", "1def..."] |
token |
"tick": "PEPE" |
(use ticks array) |
market |
"tick1": "PEPE", "tick2": "BTC" |
"pairs": [["PEPE","BTC"], ["XCHAIN","BTC"]] |
dispenser |
"action_index": "12345" |
"action_indexes": [12345, 12346] |
bet_feed |
"action_index": "12345" |
"action_indexes": [12345, 12346] |
unsubscribe
{
"action": "unsubscribe",
"channels": ["address"],
"params": { "address": "1abc..." }
}
list_subscriptions
{
"action": "list_subscriptions",
"id": "debug-1"
}
Returns a SUBSCRIPTION_LIST event with all active subscriptions and their filters.
ping
{
"action": "ping"
}
Returns a pong event. This is an application-level keepalive for aggressive proxies. The server also sends WebSocket-level ping frames every 30 seconds automatically.
Server-to-Client Events
All events share a common envelope:
{
"type": "EVENT_TYPE",
"chain": "BTC",
"network": "mainnet",
"timestamp": 1743638400000,
"schema_version": 2,
"data": { }
}
schema_version stamps every outbound frame (currently 2). Gate your
parsing on it: the server bumps it whenever any event’s data shape is
renamed, retyped, or restructured in a way an existing consumer could
mis-parse (additive optional fields do not bump it). The official SDK warns
when the server reports a newer schema than it was built against.
Wire types under schema v2: BigInt-backed fields (action_index,
block_index, block_time, latest_block_index, latest_action_index,
and other chain-index/amount fields) serialize as decimal strings, not
JSON numbers, matching the REST API. Compare them as strings or parse with
BigInt(...); a numeric === against them will silently fail. timestamp
and rate-limit metadata remain JSON numbers. The payload examples below show
the real v2 wire types.
Global Events
NEW_BLOCK
Channel: blocks
{
"type": "NEW_BLOCK",
"data": {
"block_index": "890123",
"block_hash": "00000000...",
"block_time": "1743638400",
"tx_count": 2341,
"action_count": 7
}
}
NEW_ACTION
Channel: actions
{
"type": "NEW_ACTION",
"data": {
"action_index": "45678",
"action": "SEND",
"tx_hash": "abc123...",
"block_index": "890123",
"source": "1abc...",
"status": "valid"
}
}
NETWORK_STATS
Channel: network
Emitted with each new block.
{
"type": "NETWORK_STATS",
"data": {
"block_height": 890123,
"total_actions": 45678
}
}
Entity Events
ADDRESS_UPDATE
Channel: address
{
"type": "ADDRESS_UPDATE",
"data": {
"address": "1abc...",
"balances": [
{ "tick": "XCHAIN", "amount": "1000.00000000" },
{ "tick": "PEPE", "amount": "50000.00000000" }
],
"last_action_index": 45680
}
}
TOKEN_UPDATE
Channel: token
{
"type": "TOKEN_UPDATE",
"data": {
"tick": "PEPE",
"supply": "100000000.00000000",
"holders": 1234,
"last_action_index": 45680
}
}
MARKET_UPDATE
Channel: market
{
"type": "MARKET_UPDATE",
"data": {
"tick1": "PEPE",
"tick2": "BTC",
"last_price": "0.00000020",
"volume_24h": "5000000.00000000",
"bid": "0.00000019",
"ask": "0.00000021"
}
}
DISPENSER_UPDATE
Channel: dispenser
{
"type": "DISPENSER_UPDATE",
"data": {
"action_index": "12345",
"source": "1abc...",
"status": "open",
"give_remaining": "49000.00000000",
"give_tick": "PEPE"
}
}
Betting Events
Channel: bet_feed, keyed by a market’s action_index exactly like dispenser. One subscription
follows one market through its whole life, so a market page or a wallet sees pools move live.
Filterable types are BET (one action name over all four formats: create, place, resolve, cancel)
and BET_EXPIRE (the system refund pass’s minted action).
{
"type": "BET",
"data": {
"action_index": "45900",
"feed_action_index": "45800",
"source": "1abc...",
"outcome": 1,
"amount": "250.00000000",
"tick": "PEPE"
}
}
One event on this channel has no action row behind it:
BET_CLOSED
Emitted when a market’s betting deadline latches shut. The end-of-block pass writes the status
directly and mints no action, so this rides a second cursor over bet_feeds.closed_block rather than
over actions. Without it a subscribed market page learned that betting had closed only on its next
fetch.
{
"type": "BET_CLOSED",
"data": {
"action_index": "45800",
"feed_status": "closed",
"closed_block": 962450
}
}
A coin whose indexer predates the BET tables answers “no bet_feeds table”; the explorer parks that coin’s latch cursor on a cooldown and retries, treating the gap as a deploy-order fact rather than a permanent property of the chain.
Order Lifecycle Events
These fire on both the actions channel and the address channel for involved addresses.
ORDER_MATCH
{
"type": "ORDER_MATCH",
"data": {
"action_index": "45700",
"settlement_type": "coinpay",
"status": "pending_coinpay",
"source": "1abc...",
"tx_hash": "def456..."
}
}
COINPAY_REQUIRED
Emitted when an ORDER_MATCH has settlement_type = coinpay. Contains the obligation details needed to construct a COINPAY transaction.
{
"type": "COINPAY_REQUIRED",
"data": {
"obligation_action_index": 45700,
"order_match_action_index": 45700,
"payer_address": "1BotAddr...",
"payee_address": "1SellerAddr...",
"coin_amount": "0.01000000",
"expiration": 1743642100
}
}
COINPAY_FULFILLED
{
"type": "COINPAY_FULFILLED",
"data": {
"action_index": "45720",
"tx_hash": "abc123...",
"source": "1BotAddr...",
"status": "valid"
}
}
COINPAY_EXPIRED
{
"type": "COINPAY_EXPIRED",
"data": {
"action_index": "45730",
"source": "1BotAddr...",
"status": "valid"
}
}
ORDER_EXPIRED
{
"type": "ORDER_EXPIRED",
"data": {
"action_index": "45740",
"source": "1abc...",
"status": "valid"
}
}
Swap Lifecycle Events
SWAP_MATCH
{
"type": "SWAP_MATCH",
"data": {
"action_index": "45850",
"source": "1abc...",
"status": "valid"
}
}
SWAP_EXPIRED
{
"type": "SWAP_EXPIRED",
"data": {
"action_index": "45860",
"source": "1abc...",
"status": "valid"
}
}
Dispenser Lifecycle Events
DISPENSE
{
"type": "DISPENSE",
"data": {
"action_index": "45800",
"source": "1BuyerAddr...",
"status": "valid"
}
}
DISPENSER_CLOSED / DISPENSER_EXPIRED
{
"type": "DISPENSER_CLOSED",
"data": {
"action_index": "45900",
"source": "1OwnerAddr...",
"status": "valid"
}
}
Mempool Events
Channel: mempool
Emitted when the decoder detects a new unconfirmed XChain transaction in the mempool, or when one drops out (confirmed or evicted).
MEMPOOL_ACTION
{
"type": "MEMPOOL_ACTION",
"data": {
"tx_hash": "abc123...",
"source": "1abc...",
"action": "SEND",
"data": "SEND|0|PEPE|100.00000000|1def...|"
}
}
Also broadcast on the address channel for the transaction’s source address.
MEMPOOL_REMOVED
{
"type": "MEMPOOL_REMOVED",
"data": {
"tx_hash": "abc123..."
}
}
Emitted when the mempool row is promoted to a confirmed transaction or evicted.
Attestation Events
Channel: attestation
Emitted when an ATTEST action lands in a confirmed block.
ATTESTATION_REQUEST
Emitted for ATTEST v0 (request) rows.
{
"type": "ATTESTATION_REQUEST",
"action": "ATTEST",
"channel": "attestation",
"data": { ... }
}
ATTESTATION_RESPONSE
Emitted for ATTEST v1 (response) and ATTEST v2 (system expiry) rows.
{
"type": "ATTESTATION_RESPONSE",
"action": "ATTEST",
"channel": "attestation",
"data": { ... }
}
System Events
SUBSCRIBED
Sent on every successful subscribe. Echoes the id if provided.
{
"type": "SUBSCRIBED",
"id": "sub-1",
"data": {
"channel": "address",
"address": "1abc...",
"active_filters": {
"types": ["ORDER_MATCH", "COINPAY_REQUIRED"],
"ticks": null,
"fields": null,
"once": false
}
}
}
SUBSCRIPTION_LIST
Response to list_subscriptions.
{
"type": "SUBSCRIPTION_LIST",
"id": "debug-1",
"data": {
"count": 2,
"limit": 25,
"subscriptions": [
{ "channel": "blocks", "filters": { "types": null, "once": false } },
{ "channel": "address", "address": "1abc...", "filters": { "types": ["ORDER_MATCH"], "once": false } }
]
}
}
SNAPSHOT
Sent when subscribing with snapshot: true. Contains current state of the subscribed entity.
{
"type": "SNAPSHOT",
"data": {
"channel": "address",
"address": "1abc...",
"balances": [{ "tick": "XCHAIN", "amount": "1000.00000000" }],
"last_action_index": 45680
}
}
CATCH_UP_COMPLETE
Sent after all catch-up events have been replayed.
{
"type": "CATCH_UP_COMPLETE",
"data": {
"events_replayed": 5,
"latest_action_index": "45690",
"truncated": false
}
}
Catch-up events have "catch_up": true in the envelope to distinguish them from live events.
UNSUBSCRIBED
Sent when a once: true subscription fires.
{
"type": "UNSUBSCRIBED",
"data": {
"channel": "address:1abc...",
"reason": "once"
}
}
pong
Response to client ping.
{
"type": "pong",
"timestamp": 1743638410000,
"data": {}
}
error
{
"type": "error",
"id": "sub-1",
"data": {
"code": "INVALID_TYPE",
"message": "Unknown action type: FOOBAR"
}
}
Error Codes
| Code | Meaning |
|---|---|
INVALID_ACTION |
Unrecognized client action or malformed JSON |
INVALID_CHANNEL |
Unknown channel name or missing entity params |
INVALID_CHAIN |
Unsupported coin prefix in connection URL |
INVALID_TYPE |
Unknown action type in types filter |
SUBSCRIPTION_LIMIT |
Exceeded max 25 subscriptions per connection |
RATE_LIMITED |
Client sending more than 10 messages/sec |
CATCH_UP_TOO_OLD |
since_action_index is more than 1,000 actions behind current |
CATCH_UP_IN_PROGRESS |
A catch-up request is already running for this client |
Supported Action Types for types Filter
| Category | Values |
|---|---|
| Orders | ORDER, ORDER_MATCH, ORDER_EXPIRE |
| COINPay | COINPAY, COINPAY_EXPIRE |
| Swaps | SWAP, SWAP_MATCH, SWAP_EXPIRE |
| Dispensers | DISPENSER, DISPENSE, DISPENSER_CLOSE, DISPENSER_EXPIRE |
| Transfers | SEND, SWEEP, AIRDROP, DIVIDEND |
| Tokens | ISSUE, MINT, DESTROY |
| Other | BROADCAST, CALLBACK, FILE, MESSAGE, LIST, LINK, SLEEP |
| VM | DEPLOY, EXECUTE, DEPOSIT, WITHDRAW |
| Staking | STAKE, UNSTAKE, DELEGATE, COLLECT |
| Attestation | ATTEST |
| Federation / oracle | PRICE, ANCHOR, XCALL, NODEPROOF |
| Order lifecycle | ORDER_COMPLETED, ORDER_EXPIRED |
| COINPay lifecycle | COINPAY_REQUIRED, COINPAY_FULFILLED, COINPAY_EXPIRED |
| Swap lifecycle | SWAP_COMPLETED, SWAP_EXPIRED |
| Dispenser lifecycle | DISPENSER_CLOSED, DISPENSER_EXPIRED, DISPENSER_CANCELLED |
Keepalive and Timeouts
| Behavior | Value |
|---|---|
| Server ping interval | 30 seconds (WebSocket-level, automatic) |
| Missed pong | Connection terminated immediately |
| Client with zero subscriptions | Disconnected after 5 minutes of inactivity |
| Client with active subscriptions | No timeout: stays alive indefinitely |
| Recommended client ping | Every 25 seconds (application-level {"action":"ping"}) |
Reconnection and Catch-Up
- Track
latest_action_indexfromWELCOMEand every event’saction_index - On disconnect, reconnect with exponential backoff
- Resubscribe with
since_action_indexset to your last known value - Process events with
catch_up: true(these are replayed, not live) - Wait for
CATCH_UP_COMPLETEbefore treating events as live - If
CATCH_UP_TOO_OLDerror, use the REST API to backfill
sequenceDiagram
participant C as Client
participant S as Explorer WebSocket Server
C->>S: connect
S-->>C: WELCOME
C->>S: subscribe
S-->>C: SUBSCRIBED
S-->>C: live events
Note over C,S: disconnect
C->>S: reconnect, resubscribe with since_action_index
S-->>C: replayed events
S-->>C: CATCH_UP_COMPLETE
S-->>C: live events
Configuration
See CONFIGURATION.md for the WS_* environment variables that control the WebSocket server.
| Variable | Default | Description |
|---|---|---|
WS_ENABLED |
true |
Enable/disable WebSocket server |
WS_POLL_INTERVAL |
5000 |
Change detection poll interval (ms) |
WS_PING_INTERVAL |
30000 |
Server ping interval (ms) |
WS_IDLE_TIMEOUT |
300000 |
Idle timeout for zero-subscription clients (ms) |
WS_MAX_CONNECTIONS_PER_IP |
5 |
Max concurrent connections per IP |
WS_MAX_SUBSCRIPTIONS |
25 |
Max subscriptions per connection |
WS_MAX_BACKPRESSURE |
65536 |
Max buffered bytes before skipping a client |