# WebSocket replay | API reference | Femtos

> WebSocket replay on the Femtos API: a past window of HyperCore, HyperEVM and Elysium streamed again over /v1/ws in exactly the live frames, for backtests. The URL, the controls, the replayed channels, the limits of each tier and a Python example.

- Base URL: `https://api.femtos.xyz/v1` (REST) · `wss://api.femtos.xyz/v1/ws` (WebSocket) · MCP: `https://api.femtos.xyz/mcp`
- Auth: `Authorization: Bearer $FEMTOS_KEY`. Free key: https://dashboard.femtos.xyz/signup (browser), or with
  no browser: `GET https://femtos.xyz/accounts/agent/challenge`, solve its proof of work, then `POST https://femtos.xyz/accounts/agent/signup`
  (https://femtos.xyz/agents.md#agent-onboarding)
- Decimals are exact strings, times Unix milliseconds, addresses lowercase `0x`
- Whole API in one file: https://femtos.xyz/agents.md · OpenAPI: https://api.femtos.xyz/v1/openapi.json · Index: https://femtos.xyz/llms.txt
- Page with live calls: https://femtos.xyz/reference/ws/replay

## Replay

Replay a past window over the same WebSocket, in exactly the live frames: a bot written for the live stream runs unchanged on the past. For backtests, debugging and demos.

### Connect

Add the window to the WebSocket URL. Without `replay_from` the socket is the live one.

```text
wss://api.femtos.xyz/v1/ws?ticket=$TICKET&replay_from=<ms|ISO>&replay_to=<ms|ISO>&speed=1|10|100|max
```

| Parameter |   | Meaning |
|---|---|---|
| `replay_from` | required | Start of the window: Unix ms or ISO 8601. |
| `replay_to` | optional | End of the window: Unix ms or ISO 8601. Default: `replay_from` + your tier's longest window, never past now. |
| `speed` | optional | `1` (real time, the default), `10`, `100` or `max` (as fast as you read; paid tiers only). |

The ticket is the live socket's: `POST /v1/ws-ticket` with your key (single use, valid 30 s).

### What you receive

- Subscribe as on the live socket: same channels, same filters, same `"batch":"block"`.
- Each subscription first gets its snapshot: the state at `replay_from`, with the live semantics (last candles, last trades, last events).
- The clock starts on `{"op":"replay.start"}`, or 500 ms after your last subscribe.
- Frames are the live ones plus `"replay":{"session","clock"}`. `ts.source` is the original block time, `ts.sent` the replay clock: the node's data as it happened, no network latency added.
- Events of the three layers come in time order; at the same time HyperCore, then HyperEVM, then Elysium, then by block and log index.
- The replay waits for you when you read slower than it sends: it never cuts a slow reader.

### Controls

Frames you send. Each is answered with a `replay.status` frame.

| Frame | What it does |
|---|---|
| `{"op":"replay.start"}` | Starts the clock. |
| `{"op":"replay.pause"}` | Stops the clock. The session stays open. |
| `{"op":"replay.resume"}` | Starts it again where it stopped. |
| `{"op":"replay.speed","speed":100}` | Changes the speed: `1`, `10`, `100` or `"max"`. |
| `{"op":"replay.seek","to":"2026-10-20T12:30:00Z"}` | Jumps forward to a time (Unix ms or ISO 8601): every subscription gets a fresh snapshot at the new time, then its stream. Forward only, one seek at a time: another before its `replay.status` answer is refused (`rate_limited`). |
| `{"op":"replay.status"}` | Asks where the replay is. |

Frames the server sends about the replay itself. Each carries `session` and `clock`.

| Frame | When |
|---|---|
| `{"op":"replay.started","from","to","speed"}` | The clock started. |
| `{"op":"replay.status","state","from","to","speed","effective_speed","messages","subscriptions"}` | The answer to a control. `state`: `waiting`, `running`, `paused`, `seeking` or `ended`. `effective_speed` is there when the replay runs slower than the speed asked. |
| `{"op":"replay.gap","from_block","to_block","from_time","to_time","streams"}` | A range is missing from the HyperCore tables: announced when the clock reaches it. |
| `{"op":"replay.end","to","messages","reason"}` | The end: `completed`, `paused_timeout`, `not_started`, `no_subscription` or `source_error`. The server then closes with code 1000. |

### Channels

11 channels are replayed. A subscribe to any other channel is refused with `invalid_param`.

| Channel | Layer | In a replay |
|---|---|---|
| [`hc.trades`](https://femtos.xyz/reference/ws/trades.md) | HyperCore | `filter.coin` required, as live |
| [`hc.candles`](https://femtos.xyz/reference/ws/candles.md) | HyperCore | `filter.coin` and `filter.tf` required, as live |
| [`hc.orders`](https://femtos.xyz/reference/ws/orders.md) | HyperCore | `filter.coin` required |
| [`hc.ledger`](https://femtos.xyz/reference/ws/ledger.md) | HyperCore | As live |
| [`hc.liquidations`](https://femtos.xyz/reference/ws/liquidations.md) | HyperCore | As live |
| [`hc.positions`](https://femtos.xyz/reference/ws/positions.md) | HyperCore | As live |
| [`prices.hype`](https://femtos.xyz/reference/ws/prices.md) | HyperCore | The stored samples only, see Good to know |
| [`hyperevm.trades`](https://femtos.xyz/reference/ws/trades.md) | HyperEVM | `filter.pair` required, as live |
| [`elysium.trades`](https://femtos.xyz/reference/ws/trades.md) | Elysium | `filter.pair` required, as live |
| [`elysium.launches`](https://femtos.xyz/reference/ws/launches.md) | Elysium | As live |
| [`hyperevm.launches`](https://femtos.xyz/reference/ws/launches.md) | HyperEVM | Launch rows as launched: the fields that describe the token today, such as `mcapUsd` or `holders`, are `null` |

Not replayed yet: `hc.bbo`, `hc.book`, `bridge.transfers` and `wallet`.

### Limits

Per key, by tier: the replay sessions open at once, the longest window one replays, and its fastest speed.

| Tier | Sessions at once | Longest window | Fastest speed |
|---|---|---|---|
| Free | 1 | 1 hour | 100x |
| Starter | 1 | 6 hours | max |
| Builder | 1 | 24 hours | max |
| Pro | 3 | 7 days | max |
| Scale | 10 | 30 days | max |

- Past them the upgrade answers `replay_limit` (429) or `replay_window_too_large` (400).
- `speed=max` is for paid tiers: a free key, or one past its credits, gets `replay_speed_not_allowed` (403 at the upgrade, an error frame on `replay.speed`) and replays at 100x at most.
- A session paused, not started, or running with no subscription for 5 minutes ends. A seek does not count as activity.
- The usual connection and subscription limits apply: 64 subscriptions per connection.
- The limits of your key: `GET /v1/account`, `quotas.replay`.

### Billing

A replay counts as the live socket does: each frame by its bytes (1 credit per 48 KiB, uncompressed). A `"batch":"block"` frame counts as any other: its size grows with the events it carries. The `replay.*` frames, errors and pongs count nothing.

### Good to know

- `prices.hype` replays the stored samples: the oracle and mark prices written every 30 s, and the exchange candles. What the live channel sends between them (the spot mid of each block, the oracle and mark as pushed) is not replayed.
- Order statuses (`hc.orders`) older than 7 days are read from slower storage: their replay runs at 10x at most.
- `hyperevm.trades` and `elysium.trades` need `filter.pair`, as on the live socket.
- `replay_unavailable` (503 at the upgrade): the replay engine cannot answer now, or the replays of free keys hold their whole share of it. The live socket is not affected.

### Backtest example

Python (`pip install websockets requests`): one hour of BTC 1-minute candles at 100x (`speed=max` with a paid key), a moving average crossover.

```python
import asyncio, json, os, requests, websockets

API, KEY = "https://api.femtos.xyz/v1", os.environ["FEMTOS_KEY"]

async def backtest():
    ticket = requests.post(f"{API}/ws-ticket", headers={"Authorization": f"Bearer {KEY}"}).json()["ticket"]
    url = f"wss://api.femtos.xyz/v1/ws?ticket={ticket}&replay_from=2026-10-20T12:00:00Z&replay_to=2026-10-20T13:00:00Z&speed=100"
    closes, position, pnl = [], 0, 0.0
    async with websockets.connect(url, max_size=None) as ws:
        await ws.send(json.dumps({"op": "subscribe", "channel": "hc.candles", "filter": {"coin": "BTC", "tf": "1m"}}))
        await ws.send(json.dumps({"op": "replay.start"}))
        async for raw in ws:
            m = json.loads(raw)
            if m.get("op") == "replay.end":
                break
            if m.get("channel") != "hc.candles" or m.get("snapshot"):
                continue
            c = m["data"]
            if closes and closes[-1][0] == c["t"]:
                closes[-1] = (c["t"], float(c["c"]))     # the open candle, updated
                continue
            closes.append((c["t"], float(c["c"])))
            px = [x for _, x in closes]
            if len(px) < 21:
                continue
            fast, slow = sum(px[-6:-1]) / 5, sum(px[-21:-1]) / 20
            target = 1 if fast > slow else -1
            if target != position:
                pnl -= (target - position) * px[-2]
                position = target
    print("pnl (1 BTC):", pnl + position * closes[-1][1])

asyncio.run(backtest())
```

Page: https://femtos.xyz/reference/ws/replay

Every page: https://femtos.xyz/reference.md
