# WebSockets

> Phoenix channels for real-time orders, trades, balance, and market updates.

Source: https://docs.stxapp.io/sdks/csharp/websockets/

The exchange pushes state changes over Phoenix channels. `STX.Sdk` wraps each topic in a strongly-typed class. Every channel handles:

- Authenticating the connection, by token or by signing the handshake
- Heartbeat and automatic reconnection
- Rejoining its topic after a reconnect

:::caution
User-scoped channel topics are keyed on your user id. Under API-key authentication there is no login response to read it from, so call `STXIdentityService.GetMeAsync()` once at startup **before** joining any channel below. Skipping it leaves the topic pointing at an empty id and the channel never receives anything.
:::

## Available channels

| Channel | Topic | What it pushes |
|---|---|---|
| `STXPortfolioChannel` | `portfolio:{user_id}` | Available balance, escrow, liabilities |
| `STXActiveOrdersChannel` | `active_orders:{user_id}` | Order state transitions |
| `STXActiveTradesChannel` | `active_trades:{user_id}` | Fills as they land |
| `STXActiveSettlementsChannel` | `active_settlements:{user_id}` | Settlements |
| `STXPositionsChannel` | `active_positions:{user_id}` | Position changes |
| `STXUserInfoChannel` | `user_info:{user_id}` | Account updates |
| `STXMarketChannel` | `market_info` | Market state and prices, broadcast rather than user-scoped |

## Connect a channel

Register a callback, then start it:

```csharp
var portfolio = serviceProvider.GetRequiredService<STXPortfolioChannel>();

portfolio.SetOnReceiveAction(p =>
    Console.WriteLine($"Available: {p.AvailableBalance}c  Escrow: {p.Escrow}c"));

await portfolio.StartAsync();
```

`StartAsync` opens the socket, joins the topic, and begins the heartbeat. `StopAsync` closes it. `WebSocketConnected` reports the current state, and the `SocketDisconnected` and `SocketReconnected` events fire around an automatic reconnect.

## Two ways to consume a channel

**Composition**: hand the channel a callback, as above. Best when you want each message as it lands.

**Inheritance**: subclass and override `OnReceive`:

```csharp
public class BalanceTracker : STXPortfolioChannel
{
    public BalanceTracker(STXUserStorage storage, STXEndpointSettings settings)
        : base(storage, settings) { }

    public override void OnReceive(STXPortfolio portfolio)
    {
        AvailableBalance = portfolio.AvailableBalance;
    }

    public long AvailableBalance { get; private set; }
}
```

**Wrappers**: every channel also has a `…ChannelWrapper` registered alongside it, which buffers into a bounded queue instead of calling you back. Use it when you would rather poll than handle a callback:

```csharp
var wrapper = serviceProvider.GetRequiredService<STXPortfolioChannelWrapper>();
await wrapper.StartAsync();

var latest = wrapper.LastItem;   // most recent message, or null
var all    = wrapper.Items;      // buffered messages
```

## Orders and trades

```csharp
var orders = serviceProvider.GetRequiredService<STXActiveOrdersChannel>();
var trades = serviceProvider.GetRequiredService<STXActiveTradesChannel>();

orders.SetOnReceiveAction(o =>
{
    foreach (var order in o.Orders)
        Console.WriteLine($"{order.Id}  {order.Status}  {order.Filled}/{order.Quantity}");
});

trades.SetOnReceiveAction(t =>
{
    foreach (var trade in t.Trades)
        Console.WriteLine($"Filled {trade.MarketId} at {trade.Price}c");
});

await orders.StartAsync();
await trades.StartAsync();
```

:::note
`cancelOnDisconnect: true` on an order requires `STXActiveOrdersChannel` to be joined first. The channel exposes `IsChannelConnectedAndUseCancelOnDisconnect` so you can check before placing one. Placing such an order without the channel throws `STXCancelOnDisconnectNotEnabledException`.
:::

## Market data

Market info is a broadcast, so it is not keyed on a user and works without `GetMeAsync()`:

```csharp
var market = serviceProvider.GetRequiredService<STXMarketChannel>();

market.SetOnReceiveAction(m =>
    Console.WriteLine($"{m.MarketId}  {m.Status}"));

await market.StartAsync();
```

## In a hosted service

```csharp
public class STXWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stop)
    {
        // Channel topics are keyed on the user id. Under API-key auth there is no
        // login response, so fetch it once before joining anything.
        await _identity.GetMeAsync();

        _portfolio.SetOnReceiveAction(p => _balance = p.AvailableBalance);   // long, in cents
        _orders.SetOnReceiveAction(HandleOrders);

        await _portfolio.StartAsync();
        await _orders.StartAsync();

        await Task.Delay(Timeout.Infinite, stop);
    }

    public override async Task StopAsync(CancellationToken stop)
    {
        await _orders.StopAsync();
        await _portfolio.StopAsync();
        await base.StopAsync(stop);
    }
}
```

Channels are registered as singletons, so the same instance is shared across your app. Resolve them once and keep them.

## See also

- [Trading](/sdks/csharp/trading/): placing the orders these channels report on
- [Settlements](/sdks/csharp/settlements/): settlement history and its channel
