Skip to main content

Realtime Stream

The SDK includes a client for the gSwap realtime stream (gdex WebSocket Protocol v1): public, read-only queries and live events over strict JSON-RPC 2.0 on one raw WebSocket. It is not a Socket.IO endpoint.

Connect​

GSwap creates the client for its environment the first time you use gSwap.stream. Nothing connects until you call connect():

import { GSwap } from '@gala-chain/gswap-sdk';

const gSwap = new GSwap({ env: 'stage' });
await gSwap.stream.connect();

console.log(await gSwap.stream.serverInfo());
const blocks = await gSwap.stream.subscribeBlocks((event) => console.log(event.blockNumber), { snapshot: true });

await blocks.unsubscribe();
gSwap.stream.disconnect();

You can also create it directly when you only need the stream:

import { GdexStreamService } from '@gala-chain/gswap-sdk';

const stream = new GdexStreamService({ url: 'wss://dex-backend-prod1.defi.gala.com/ws' });
await stream.connect();
EnvironmentURL
prodwss://dex-backend-prod1.defi.gala.com/ws
stageThe staging swap backend's /ws (set by the env: 'stage' preset)

With a custom dexBackendBaseUrl, the stream URL is derived from it (https becomes wss, plus /ws) unless you pass dexBackendWsUrl. All on-chain numbers and epoch-millisecond timestamps stay decimal strings: do not round-trip them through JavaScript numbers.

Call disconnect() at shutdown. It closes on purpose and turns automatic reconnect off.

Channels​

Each subscription method returns a handle with subscriptionId and unsubscribe(), and your callback receives each event's payload.

ChannelMethodSends
swapsubscribeSwaps(cb, opts?)one event per committed swap (omit the pool for all pools)
liquiditysubscribeLiquidity(cb, opts?)one event per committed add, remove or collect
poolsubscribePool(selector, cb, opts?)state changes of one pool (snapshot has the full tick map)
balancesubscribeBalances(owner, cb, opts?)balance changes of one wallet
poolCreatedsubscribePoolCreated(cb, opts?)each newly created pool
blockssubscribeBlocks(cb, opts?)each block-height advance

Which channel tells me who did something?​

I want to know…UseField that answers it
who added or removed liquidity, or collected feesliquidityevent.owner
who made a swapswapevent.sender
the pool's price, tick, reserves or liquidity nowpoolsqrtPrice, tick, token0Reserve
how one wallet's balance changedbalancequantity
that a pool was created, and by whompoolCreatedcreator

pool events never name the wallet that caused them. A pool event is the pool's state after something touched it, and one event can cover several transactions in the same block. On v2 its creator is the wallet that created the pool, which never changes; it is not whoever just traded or added liquidity. A bot that announces "wallet X added liquidity to pool Y" should subscribe to liquidity for the wallet and amounts, and use pool only for the resulting numbers. Match the two on transactionId.

Swaps sent in shadow mode (X-Shadow: true) still appear on swap and move the pool, with the wallet withheld: sender is the marker <private> (the same one the chain's public query-api uses; it is not an address, so code that validates sender must allow it), and transactionId, applyTransactionId, requestTransactionId and uniqueKey are opaque ids. They are stable per swap, so you can still deduplicate, but they do not resolve on a block explorer. The swapper gets no balance event. A sender of <private> means "wallet withheld", not a real user, so leave it out of per-wallet statistics.

Pass snapshot: true to receive current state right after the subscribe response, and fields to trim each event (identity and envelope fields always stay). Events carry version: 'v1' (the exit-only legacy contract) or 'v2' (the live contract); an unfiltered subscription receives both.

Liquidity: add, remove, collect​

liquidity reports every committed AddLiquidity, RemoveLiquidity and CollectPositionFees, on both contract versions. They never appear on swap. Every filter is optional; with none you receive every liquidity event.

import { GSwap } from '@gala-chain/gswap-sdk';

const gSwap = new GSwap();
await gSwap.stream.connect();

// Every liquidity change by one LP wallet:
await gSwap.stream.subscribeLiquidity(
(event) => console.log(event.action, event.poolHash, event.amount0, event.amount1),
{ owner: 'client|618ae395c1c653111d3315be' },
);

// Every liquidity change in one pool:
await gSwap.stream.subscribeLiquidity((event) => console.log(event), { poolHash: 'v2:GALA$GUSDC$10000' });

Filters: pool or poolHash, owner (the position's owner), transactionId, version ('v1' | 'v2' | 'all'). There is no snapshot, because an event reports a committed operation, not current state; read positions with getPositions().

FieldMeaning
actionadd, remove or collect
ownerthe position's owner (the LP wallet)
version, pool, poolHash, transactionId, blockNumber, timestampidentity and envelope, always present
amount0, amount1principal moved: deposited by an add, withdrawn by a remove, "0" for a collect
fees0, fees1fees paid out. v2: always present ("0" for an add). v1: present on a collect only
positionIdv1: the position id. v2: the chain's position key <pool>|<tickLow>|<tickHigh>|<owner>
tickLower, tickUpperthe position's range
liquidityAfterv2 only: the position's liquidity after the operation; "0" means it is closed
token0Symbol, token1Symbolv2 only

A v2 add:

{
"version": "v2",
"pool": { "token0ClassKey": "MCA-imzqovym|Unit|testfaxcjykgnoki|none", "token1ClassKey": "MCB-imzqovym|Unit|testckehdbwxnkmv|none", "fee": 3000 },
"poolHash": "v2:MCAIMZQOVYM$MCBIMZQOVYM$3000",
"transactionId": "5f1c…",
"action": "add",
"owner": "client|lp1-mnqszsfwcj",
"positionId": "MCAIMZQOVYM$MCBIMZQOVYM$3000|0884392|0888232|client|lp1-mnqszsfwcj",
"tickLower": -2880,
"tickUpper": 960,
"liquidityAfter": "50505.195635588461491346",
"amount0": "3025.72277487",
"amount1": "6122.68379996",
"fees0": "0",
"fees1": "0",
"token0Symbol": "MCAIMZQOVYM",
"token1Symbol": "MCBIMZQOVYM",
"blockNumber": 2459000,
"timestamp": "1787928959212"
}

The same operation still updates the pool channel (new liquidity, reserves, price and tick, with no wallet and no amounts) and, for a subscribed owner, the balance channel. Use liquidity to learn what happened and pool and balance to track the resulting state. A malformed liquidity frame is dropped, never passed to your callback.

Deduplicate by transactionId plus positionId: a v1 BatchSubmit can hold several operations under one transaction id.

Queries​

The client also answers the read-only protocol queries. request() is the escape hatch for anything else, such as gdex_listSubscriptions.

import { GSwap } from '@gala-chain/gswap-sdk';

const stream = new GSwap({ env: 'stage' }).stream;
await stream.connect();

const pool = { token0ClassKey: 'GALA|Unit|none|none', token1ClassKey: 'GUSDC|Unit|none|none', fee: 10000 };
const info = await stream.serverInfo();
const state = await stream.getPool({ pool });
const balance = await stream.getBalance('client|618ae395…', 'GUSDC|Unit|none|none');
const positions = await stream.getPositions({ owner: 'client|618ae395…', pool, limit: 100 });
const height = await stream.getBlockHeight();
const quote = await stream.quote({ tokenIn: 'GALA|Unit|none|none', tokenOut: 'GUSDC|Unit|none|none', amountIn: '100' });

quote takes exactly one of amountIn or amountOut; omit fee to compare every tier. Each request times out after 15 seconds by default. A JSON-RPC error rejects with GdexStreamError, which keeps code, message and data.

Reconnect and ordering​

An unexpected close reconnects with exponential backoff (five attempts, starting at two seconds, by default). Every active handle is resubscribed with snapshot: true, its subscriptionId updates, and the same callback and unsubscribe() keep working. Rate-limit responses are retried using data.retryAfterMs.

There is no server cursor, so treat the reconnect snapshot as truth. Delivery is at least once: roughly 40 blocks can replay after a server restart. Dedupe swaps by transactionId; for state channels keep the value with the later blockNumber, which is non-decreasing within one subscription. Expect about two seconds from block commit to event.

Limits and errors​

16 connections per IP, 256 subscriptions per connection, 20 requests per rolling 10 seconds (subscribe and unsubscribe count), 1 MiB frames, 20-call batches, and a 4 MiB buffer before a slow consumer is disconnected. -32003 means rate limited (see data.retryAfterMs), -32005 unknown pool, -32006 unknown token, -32001 invalid channel params, -32002 subscription limit.

Node.js before version 22​

Browsers and Node.js 22 or newer use globalThis.WebSocket. On older Node.js, pass a WebSocket constructor yourself, for example from the ws package; the SDK does not bundle one. gSwap.stream only needs it when you first use it.

import WebSocket from 'ws';
import { GSwap } from '@gala-chain/gswap-sdk';

const gSwap = new GSwap({ env: 'stage', webSocketImpl: WebSocket });
await gSwap.stream.connect();

Authentication​

Public channels need none. A privileged connection (shadow swaps with the wallet unredacted) is selected by an x-api-key header on the WebSocket upgrade request, which the server reads from no other place. Browser and React Native WebSockets are therefore split: a browser cannot set the header and always sees the public view; Node.js (ws) and React Native accept { headers } as the constructor's third argument, which is what apiKey uses. It requires webSocketImpl, is meant for trusted server-side code, and is described in App and Wallet Integration.

import { GSwap } from '@gala-chain/gswap-sdk';
import { WebSocket } from 'ws';

const gSwap = new GSwap({ webSocketImpl: WebSocket, streamApiKey: process.env.GDEX_API_KEY });