> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tensorcost.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-time events

> Subscribe to live tenant-scoped events via socket.io — recommendations, alerts, agent state, runaway loops, ML training. Plus the MCP server.

# Real-time events

The TensorCost backend streams live events to the dashboard, partner integrations, and any authenticated client over **Socket.IO**. Events cover the full lifecycle of the platform — new metrics arriving, instances changing state, alerts firing, ML training, and the runaway-loop detector pausing an agent.

<Warning>
  This describes the current design and the code path that implements it. If you connect and get a flat "not found" instead of a Socket.IO handshake response, that's a live rollout gap on our side, not a mistake in your client — page support and we'll confirm status for your tenant.
</Warning>

## Where the WebSocket lives

Real-time events are Socket.IO traffic on the **same host** as the console app (there's no separate `ws.` subdomain) — it's routed to the backend through a dedicated `/rt/*` path so the upgrade request reaches the gateway instead of falling through to the static frontend.

| Traffic                         | Hostname                                          | Path                               | Port        |
| ------------------------------- | ------------------------------------------------- | ---------------------------------- | ----------- |
| Frontend + REST + real-time     | your console host (e.g. `console.tensorcost.com`) | `/rt` (Socket.IO), `/api/*` (REST) | 443         |
| gRPC (agents, opt-in transport) | shown in the console when you enable it           | —                                  | 50051 (TLS) |

## Connecting

```js theme={null}
import { io } from 'socket.io-client';

const socket = io('https://<your-console-host>/rt', {
  path: '/rt',
  withCredentials: true, // browser: sends the httpOnly session cookie set at sign-in
  transports: ['websocket'],
});

socket.on('event', (event) => {
  console.log(event.type, event.data);
});
```

### Authentication

* **Browser clients** — the handshake carries your session's httpOnly cookie automatically (`withCredentials: true`); there's nothing to pass explicitly. This is what the console itself uses.
* **Non-browser clients** (a script, a service) — pass a bearer token explicitly instead of relying on a cookie. Check with support for the current token-issuance path if you're building outside the browser.

Clients are joined server-side to a room scoped to their own tenant, resolved from their session — never from anything the client sends. Cross-tenant events are unreachable by construction.

## Event envelope

Every message arrives on the `event` channel with a uniform shape:

```json theme={null}
{
  "type": "ml.training.completed",
  "data": {
    "model_type": "gpu_anomaly",
    "duration_ms": 7512,
    "model_id": 42
  },
  "timestamp": "2026-04-29T16:41:38.412Z",
  "trace_id": "7a0db7e045..."
}
```

| Field       | Description                                                                          |
| ----------- | ------------------------------------------------------------------------------------ |
| `type`      | Dotted event name; the prefix is the domain (`instance.*`, `recommendation.*`, etc.) |
| `data`      | Event-specific payload                                                               |
| `timestamp` | ISO 8601 publish time                                                                |
| `trace_id`  | OTel trace ID for cross-system correlation                                           |

## Event catalog

| Prefix                              | Example events                                                                                       | Typical use                                           |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `instance.*`                        | `instance.created`, `instance.stopped`, `instance.state_changed`                                     | Invalidate the instances list, refresh fleet topology |
| `metrics.*`                         | `metrics.collected`, `metrics.processed`, `metrics.anomaly_detected`                                 | Refresh sparklines, flash an anomaly badge            |
| `cost.*`                            | `cost.updated`, `cost.threshold_exceeded`, `cost.forecast_generated`, `cost.budget_breach_predicted` | Update cost charts and budget gauges                  |
| `alert.*`                           | `alert.created`, `alert.resolved`, `alert.escalated`, `alert.suppressed`                             | Inbox UIs, PagerDuty bridges                          |
| `agent.*`                           | `agent.connected`, `agent.disconnected`, `agent.error`, `agent.health_check`                         | Agent fleet health                                    |
| `recommendation.*`                  | `recommendation.created`, `recommendation.accepted`, `recommendation.dismissed`                      | Recommendations feed                                  |
| `runaway_loop.*`                    | `runaway_loop.detected`, `runaway_loop.paused`, `runaway_loop.resolved`                              | Agent cost pager                                      |
| `inference.*`                       | `inference.spike_detected`, `inference.cache_miss_surge`                                             | Bedrock / Azure OpenAI dashboards                     |
| `action.*`                          | `action.queued`, `action.approved`, `action.executing`, `action.completed`, `action.rolled_back`     | Enforcement queue                                     |
| `notification.*`                    | `notification.requested`, `notification.sent`, `notification.failed`                                 | Trace alert delivery                                  |
| `incident.*`, `security_incident.*` | `incident.created`, `security_incident.escalated`                                                    | Incident response timeline                            |
| `spot.*`                            | `spot.interruption_detected`, `spot.fallback_initiated`                                              | AWS spot handling                                     |
| `scaling.*`                         | `scaling.action_triggered`, `scaling.completed`                                                      | Auto-scaling surface                                  |
| `ml.*`                              | `ml.training.started`, `ml.training.completed`, `ml.training.failed`                                 | Live retraining progress                              |
| `tenant.*`, `policy.*`, `system.*`  | Housekeeping                                                                                         | Audit log enrichment, admin UIs                       |

## Using events in a React app

The shell pairs Socket.IO with TanStack Query (and legacy RTK Query during the migration). When the backend publishes an event, the client invalidates the right query key so any subscribed component refetches:

```ts theme={null}
const PREFIX_TAG_MAP: Record<string, string[]> = {
  'instance.':       ['instance'],
  'metrics.':        ['metric'],
  'cost.':           ['cost'],
  'alert.':          ['alert'],
  'recommendation.': ['recommendation'],
  'runaway_loop.':   ['agent', 'recommendation'],
  'ml.':             ['ml-model'],
};

socket.on('event', (e) => {
  const tags = tagsForEvent(e.type);
  if (tags) queryClient.invalidateQueries({ queryKey: tags });
});
```

For more targeted UI (a progress banner, a toast), components can subscribe via a hook like `useRunawayLoopEvents()` that filters the stream and exposes only the relevant slice.

## Reliability guarantees

* **In-process priority.** Events dispatch to local subscribers synchronously in the publish path with no Redis dependency. A single-instance deployment works end-to-end with no broker.
* **Redis for durability** (when configured). Events are also `LPUSH`'d onto per-type queues for cross-instance replay. A bounded LRU of recently-dispatched event IDs prevents double delivery.
* **Best-effort on Redis failure.** If Redis is unreachable, events still reach all in-process subscribers (and thus the Socket.IO bridge). Only cross-instance replay is lost.
* **Per-type ordering.** Events within a single type are ordered. Across types, no ordering guarantees.
* **Per-tenant rate limits.** Socket.IO emit rate is capped per tenant to protect noisy-neighbor scenarios. Default 100 events/sec/tenant; raise on enterprise tier.

## Webhooks

Every event in the catalog can also be delivered as a webhook (HMAC-signed `X-TensorCost-Signature: t=<unix>,v1=<sig>`). Configure under **Integrations → Webhooks**. Webhooks complement Socket.IO for systems that prefer pull-once-and-acknowledge delivery (PagerDuty, ServiceNow, internal eventing pipelines).

## Debugging connection issues

Common causes of a failed handshake:

* **Wrong path.** The upgrade must go to `/rt` on your console host, not a separate subdomain and not the Socket.IO default path (`/socket.io/`).
* **Expired session.** If your cookie or token has expired, reconnect after re-authenticating. The console handles this automatically on route navigation.
* **A proxy stripping `Connection: Upgrade` / `Upgrade: websocket` headers.** If you're connecting through a corporate proxy, confirm it forwards WebSocket upgrade headers on `/rt`.

## MCP

TensorCost exposes an **MCP endpoint** at `/mcp` on your console host, so Claude Desktop, internal LLM agents, and partner integrations can query the platform programmatically. See [developer tools](/developer-tools#mcp) for setup and [the CLI](/cli) if you just want cost numbers from a terminal without wiring up MCP.

<Note>
  Rolling out to production tenants — if `/mcp` isn't reachable on your console host yet, ask support whether it's enabled for your tenant.
</Note>

### Tool surface

| Tool                     | Scope required          | Returns                                                                     |
| ------------------------ | ----------------------- | --------------------------------------------------------------------------- |
| `get_cost_summary`       | `read:costs`            | Top-line cost for the tenant — total, trailing N days, breakdown by service |
| `get_cost_by_feature`    | `read:costs`            | Spend attributed by feature tag, optionally filtered to one feature         |
| `list_recommendations`   | `read:costs`            | Active recommendations                                                      |
| `list_gpu_instances`     | `read:gpu`              | GPU fleet inventory                                                         |
| `get_instance_metrics`   | `read:gpu`              | Time-series metrics for one instance                                        |
| `get_fleet_health`       | `read:gpu`              | Agent connection status across the fleet                                    |
| `list_ai_workloads`      | `read:workloads`        | Per-application / per-team AI workload list                                 |
| `get_workload_cost`      | `read:workloads`        | Cost for one workload                                                       |
| `list_anomalies`         | `read:anomalies`        | Detected anomalies, filterable                                              |
| `get_inference`          | `read:anomalies`        | Detail on one inference/anomaly record                                      |
| `accept_inference`       | `write:anomalies`       | Accept a recommendation tied to an anomaly                                  |
| `reject_inference`       | `write:anomalies`       | Reject a recommendation tied to an anomaly                                  |
| `dismiss_recommendation` | `write:recommendations` | Dismiss a recommendation with a reason                                      |

### Scope-guarded by RBAC

Every tool call is scope-checked against the calling key. Read scopes are available on all plan tiers; write scopes require an explicit grant.

### Authenticating

MCP clients authenticate with an API key, sent as the `X-MCP-Key` header — this is a different credential from your REST bearer token. Mint one under **Settings → MCP**.

### Use cases

* **CFO / finance** — point Claude Desktop at TensorCost MCP and ask "what drove last week's Bedrock bill?" The model uses `get_cost_summary` + `list_recommendations` to compose the answer.
* **Platform engineer** — wire your internal agent to MCP to check fleet health or workload cost during an incident.
* **Partner integration** — build a Datadog / Grafana panel that consumes `get_cost_summary` and `get_fleet_health` for embedded TensorCost views.

The MCP endpoint is rate-limited per key. The same tenant-scoping guarantees that apply to REST apply here too.
