# SignalR hubs

> Subscribe to Entropia Central's public real-time feeds for globals, trade messages and PvP actions over SignalR.

Source: https://www.entropiacentral.com/documentation/api-reference/signalr-hubs
Last updated: 2026-07-28

---

Entropia Central broadcasts live activity over [SignalR](https://learn.microsoft.com/aspnet/core/signalr/introduction). Three feeds are public and open to anyone: globals, trade messages and PvP actions. Connect to a hub, listen for its event, and the server pushes records as they are recorded.

These feeds are read only. The hubs push data to you and there is nothing to send back beyond the subscription calls described below.

## Available hubs

All hubs live under `https://api.entropiacentral.com`.

| Hub | Path | Event you listen for | What it carries |
| --- | --- | --- | --- |
| Globals | `/hubs/globals` | `ReceiveGlobal` | Globals, HOFs and ATHs as they are recorded |
| Trade messages | `/hubs/trademessages` | `ReceiveTradeMessage` | Messages from the in-game trade channels |
| PvP actions | `/hubs/pvpactions` | `ReceivePvpAction` | PvP kills |

## Connecting

Build a connection, register your handler, then start it. Register handlers before calling `start()` so you do not miss records that arrive during the handshake.

```ts
import { HubConnectionBuilder } from '@microsoft/signalr';

const connection = new HubConnectionBuilder()
  .withUrl('https://api.entropiacentral.com/hubs/globals')
  .withAutomaticReconnect()
  .build();

connection.on('ReceiveGlobal', (global) => {
  console.log(global.avatarName, global.globalValue, global.type);
});

await connection.start();
```

Payload properties are camelCased on the wire, so the C# property `AvatarName` arrives as `avatarName`.

### Globals and trade messages start streaming immediately

Connecting to the globals hub or the trade messages hub is enough. The server adds you to that hub's broadcast group as part of the connection, so records begin arriving as soon as `start()` resolves.

### PvP actions require an explicit join

The PvP hub is the exception. Connecting alone delivers nothing. Call `JoinPvpActions` after starting:

```ts
const connection = new HubConnectionBuilder()
  .withUrl('https://api.entropiacentral.com/hubs/pvpactions')
  .withAutomaticReconnect()
  .build();

connection.on('ReceivePvpAction', (action) => {
  console.log(action.attacker, 'killed', action.defender);
});

await connection.start();
await connection.invoke('JoinPvpActions');
```

Call `LeavePvpActions` to stop receiving them without dropping the connection. Re-invoke `JoinPvpActions` after a reconnect, because group membership does not survive one.

## Narrowing what you receive

Each hub accepts subscription calls that add you to a narrower group.

### Globals

| Call | Argument | Effect |
| --- | --- | --- |
| `SubscribeToGlobalType` | type string, for example `hof` | Globals of that type only |
| `UnsubscribeFromGlobalType` | type string | Stops the above |
| `SubscribeToEvent` | numeric event id | Globals scored towards that community event |
| `UnsubscribeFromEvent` | numeric event id | Stops the above |

### Trade messages

| Call | Argument | Effect |
| --- | --- | --- |
| `SubscribeToChannel` | channel name, for example `trade` | Messages on that channel only |
| `UnsubscribeFromChannel` | channel name | Stops the above |

### PvP actions

| Call | Argument | Effect |
| --- | --- | --- |
| `JoinPvpActions` | none | All PvP actions |
| `JoinAttacker` | avatar name | Actions where that avatar is the attacker |
| `JoinDefender` | avatar name | Actions where that avatar is the defender |
| `LeavePvpActions`, `LeaveAttacker`, `LeaveDefender` | matching argument | Stops the above |

Avatar and channel names are matched case insensitively.

## Subscribing causes duplicate deliveries

This is the one behaviour that surprises people. A narrower subscription does not replace the broad feed, it adds a second delivery path, and the server broadcasts to every matching group.

On the globals hub you are already in the broadcast group from the moment you connect. If you then call `SubscribeToGlobalType('hof')`, a HOF is sent to the broad group and to the type group, so your `ReceiveGlobal` handler fires twice for the same record. Trade message channels and PvP attacker and defender groups behave the same way.

Two ways to deal with it:

- Use the broad feed and filter client side, which is the simplest option and what the Entropia Central site itself does.
- Or subscribe narrowly and deduplicate. Globals carry `strippedMessage`, which is the value the server itself hashes for deduplication, and trade messages carry a stable `id`.

Subscribing to a specific attacker and defender who appear in the same kill will deliver that action three times: once for the broad group and once for each name group.

## Payloads

### ReceiveGlobal

| Property | Type | Notes |
| --- | --- | --- |
| `avatarName` | string | Avatar that recorded the global |
| `globalValue` | number | Value in PED |
| `type` | string | Global category, for example hunting or mining |
| `dateTime` | string | UTC timestamp, always set by the server |
| `isHof`, `isAth`, `isTeam` | boolean | Classification flags |
| `fullMessage` | string | Original in-game message |
| `strippedMessage` | string | Message without timestamp or channel prefix, used for deduplication |
| `creatureName`, `depositName`, `instanceName`, `craftedItemName`, `landareaName`, `rareItemName`, `discoveredItemName`, `tieredItemName` | string | Populated according to `type`, empty otherwise |
| `tieredItemTier` | number | Tier reached, for tiering globals |
| `pvpSpree` | number | Kill count, for PvP globals |
| `detailRoute` | string or null | Wiki path for the subject, for example `/wiki/creatures/some-slug` |

The name fields are always present and are empty strings when they do not apply, rather than being omitted.

### ReceiveTradeMessage

| Property | Type | Notes |
| --- | --- | --- |
| `id` | number | Stable identifier, useful for deduplication |
| `channel` | string | Originating in-game channel |
| `author` | string | Avatar that posted |
| `content` | string | Message text |
| `dateTime` | string | UTC timestamp, most recent posting when reposts were collapsed |
| `repeatCount` | number | 1 for a single posting, higher when identical reposts were collapsed |
| `firstPostedAt` | string or null | First posting time, set only when reposts were collapsed |

### ReceivePvpAction

| Property | Type | Notes |
| --- | --- | --- |
| `attacker` | string | Avatar that got the kill |
| `defender` | string | Avatar that was killed |
| `weapon` | string | Weapon used |
| `dateTime` | string | UTC timestamp |

## Reconnects

Use `withAutomaticReconnect()`. Two things to keep in mind when a connection drops and comes back:

**Nothing is replayed.** Records broadcast while you were disconnected are gone. If you need an unbroken series, re-fetch the current state from the REST endpoints on reconnect and treat the socket as live updates only.

**Group membership is not restored.** Automatic group joins are re-applied by the server, so globals and trade messages resume on their own. Anything you joined by calling a method, including every PvP subscription, has to be re-invoked:

```ts
connection.onreconnected(async () => {
  await connection.invoke('JoinPvpActions');
});
```
