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

# Market making with heartbeat protection

> Run an unattended quoting loop inside a heartbeat so a crash cannot leave exposure resting on the book.

Any unattended strategy should sit inside a heartbeat. If the process dies, the heartbeat expires and the server closes every open order on the account.

## The shape

```
create heartbeat (timeout T)
loop:
  refresh heartbeat every T/3          on its own timer
  read the book                        from the stream
  decide quotes
  place new orders                     each with a fresh request_uuid
  close orders that no longer fit
  close betslips you stop quoting      DELETE /v2/betslips/{betslip_id}/
on exit:
  close open orders
  delete heartbeat
```

## Choosing the timeout

The valid range is 10 to 300 seconds.

* **Short (10 to 30 s).** Tight protection. A slow tick risks a self-inflicted closure. Refresh at `T/3`.
* **Long (120 to 300 s).** Tolerant of hiccups. Leaves exposure open longer after a genuine crash.

Refresh on a timer **independent of your trading loop**. If refreshing is coupled to a loop that can block on a slow REST call, a stall expires the heartbeat and closes your book.

## The code

```python theme={"dark"}
import os, json, time, uuid, threading, requests

API = "https://magicmarkets.com/v2"
H = {"X-Api-Key": os.environ["MAGICMARKETS_API_KEY"]}
T = 60
SPORT = "fb"  # the sport this strategy quotes

def post(path, body=None):
    r = requests.post(f"{API}{path}", headers=H, json=body, timeout=10).json()
    if r["status"] != "ok" and r.get("code") != "order_already_created":
        raise RuntimeError(r)
    return r["data"]

hb = post("/heartbeats/", {"timeout": T})["heartbeat_id"]
stop = threading.Event()

def keep_alive():
    while not stop.wait(T / 3):
        try:
            post(f"/heartbeats/{hb}/refresh/")
        except Exception as e:
            print("refresh failed:", e)

threading.Thread(target=keep_alive, daemon=True).start()

try:
    while True:
        # 1. read your book from the stream (see the price book guide)
        # 2. decide the quotes you want resting
        # 3. quote: betslip -> pmm -> order, always with a request_uuid
        req = str(uuid.uuid4())
        # order = post("/orders/", {"betslip_id": ..., "price": ..., "stake": ["USDT", 10.0],
        #                           "duration": 30.0, "request_uuid": req})
        # 4. close orders that no longer fit
        # 5. close betslips you stop quoting:
        #    requests.delete(f"{API}/betslips/{betslip_id}/", headers=H)
        time.sleep(1)
finally:
    stop.set()
    post("/orders/close_all/", {"sport": SPORT})
    requests.delete(f"{API}/heartbeats/{hb}/", headers=H)
```

On exit, `close_all/` with a `sport` closes every open order on that sport in one call, with no paging. It also closes orders on that sport that other programs on the account placed. If several strategies share a sport, list the open orders page by page and close your own with `close_many/`. See [Bulk close](/developers/guides/bulk-close).

Close betslips you stop quoting with `DELETE /v2/betslips/{betslip_id}/`; an account can hold only a limited number open.

## Idempotency under retry

Always send a `request_uuid`. Under retry, a reused UUID returns the error code `order_already_created` with the existing `order_id`. Without one, a timeout followed by a retry can double-place. Persist the UUID before you send, so a crash between send and response is recoverable through `GET /v2/orders/tracked/{uuid}/`.

## Rate limits

Order placement has its own budget of 5 requests per second. Betslip creation has its own budget of 10 requests per second and 2,000 betslips per rolling 24 hours. Every create counts, including a rejected one. A refresh does not count. The budget refills continuously, with no fixed reset. Both are per account, across all keys. A quoting loop that re-quotes every selection every tick will hit them. Re-quote only what moved. See [Errors and rate limits](/developers/errors-and-rate-limits).

## Test first

Run the loop with a small stake before you leave it unattended. The [Claude skill repository](/developers/tools/claude-skill) has a runnable single-order version with heartbeat protection, `03-market-making.py`. It is dry-run by default.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.