API reference, Market data
WebSocket 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.
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.sourceis the original block time,ts.sentthe 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 | HyperCore | filter.coin required, as live |
| hc.candles | HyperCore | filter.coin and filter.tf required, as live |
| hc.orders | HyperCore | filter.coin required |
| hc.ledger | HyperCore | As live |
| hc.liquidations | HyperCore | As live |
| hc.positions | HyperCore | As live |
| prices.hype | HyperCore | The stored samples only, see Good to know |
| hyperevm.trades | HyperEVM | filter.pair required, as live |
| elysium.trades | Elysium | filter.pair required, as live |
| elysium.launches | Elysium | As live |
| hyperevm.launches | 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) orreplay_window_too_large(400). speed=maxis for paid tiers: a free key, or one past its credits, getsreplay_speed_not_allowed(403 at the upgrade, an error frame onreplay.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.hypereplays 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.tradesandelysium.tradesneedfilter.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.
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())More
- WebSocket streams: the live channels, each with its filter and frames.
- Plans and limits: what each tier can call and replay.
- Error codes:
replay_limit,replay_window_too_large,replay_speed_not_allowed,replay_unavailable. - This page as Markdown
- agents.md: the same text, with the whole API
Agent guide (agents.md)llms.txtOpenAPIMCP serverGet an API keyChangelog