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();
| Environment | URL |
|---|---|
prod | wss://dex-backend-prod1.defi.gala.com/ws |
stage | The 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.
| Channel | Method | Sends |
|---|---|---|
swap | subscribeSwaps(cb, opts?) | one event per committed swap (omit the pool for all pools) |
liquidity | subscribeLiquidity(cb, opts?) | one event per committed add, remove or collect |
pool | subscribePool(selector, cb, opts?) | state changes of one pool (snapshot has the full tick map) |
balance | subscribeBalances(owner, cb, opts?) | balance changes of one wallet |
poolCreated | subscribePoolCreated(cb, opts?) | each newly created pool |
blocks | subscribeBlocks(cb, opts?) | each block-height advance |
Which channel tells me who did something?
| I want to know… | Use | Field that answers it |
|---|---|---|
| who added or removed liquidity, or collected fees | liquidity | event.owner |
| who made a swap | swap | event.sender |
| the pool's price, tick, reserves or liquidity now | pool | sqrtPrice, tick, token0Reserve |
| how one wallet's balance changed | balance | quantity |
| that a pool was created, and by whom | poolCreated | creator |
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().
| Field | Meaning |
|---|---|
action | add, remove or collect |
owner | the position's owner (the LP wallet) |
version, pool, poolHash, transactionId, blockNumber, timestamp | identity and envelope, always present |
amount0, amount1 | principal moved: deposited by an add, withdrawn by a remove, "0" for a collect |
fees0, fees1 | fees paid out. v2: always present ("0" for an add). v1: present on a collect only |
positionId | v1: the position id. v2: the chain's position key <pool>|<tickLow>|<tickHigh>|<owner> |
tickLower, tickUpper | the position's range |
liquidityAfter | v2 only: the position's liquidity after the operation; "0" means it is closed |
token0Symbol, token1Symbol | v2 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 });