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

# Alert bot

> Send large-trade alerts to Slack and save a checkpoint so the consumer can resume after failure.

Send Slack alerts for recorded large Polymarket trades from S- and A-grade wallets. Save the last completed event after Slack acknowledges the alert, so a restart resumes from that point.

This consumer delivers at least once: a crash after Slack accepts an alert but before the checkpoint is saved can send the same alert again. The example also stops progress when trade details are unavailable, rather than silently skipping the event.

You need a [Pro API key](/authentication), Python with `requests`, and a Slack incoming webhook URL. Run `python -m pip install requests`, then set `OXINSIDER_API_KEY` and `SLACK_WEBHOOK_URL` in your environment. Combine steps 2 through 6 into one script in their displayed order.

<span id="1-how-the-bot-hears-about-a-new-trade" />

## 1. Choose how to receive events

Use [Event replay](/api-reference/endpoint/get-event-replay-since) to read trades after a saved cursor. A webhook or SSE connection can wake the consumer sooner, but the replay cursor determines which trades still need processing.

| Method | What it does here |
| - | - |
| Event replay | Returns stored trade events and, with `expand=trade`, trade details. |
| Webhook | Notifies the consumer that new trades were recorded. A public HTTPS destination is required. |
| SSE stream | Notifies the consumer over an open connection. No public callback URL is required. |

Start with polling alone. Add a webhook or stream if you need lower notification latency.

<span id="3-the-checkpoint" />

## 2. Save a local checkpoint

Store the last completed event's `cursor` and recent delivered event IDs in a file. `os.replace` replaces the file atomically, so a process interruption does not leave a partially written JSON document.

```python theme={null}
import json
import os
import threading
import time

import requests

BASE = "https://api.0xinsider.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['OXINSIDER_API_KEY']}"}
STATE_FILE = "alert-bot-state.json"
DELIVERED_KEEP = 1000


def load_state():
    try:
        with open(STATE_FILE) as f:
            return json.load(f)
    except FileNotFoundError:
        return {"cursor": None, "delivered": []}


def save_state(state):
    tmp = STATE_FILE + ".tmp"
    with open(tmp, "w") as f:
        json.dump(state, f)
    os.replace(tmp, STATE_FILE)
```

The first run has no cursor and starts with recent events. It does not read all historical trades. To begin from an existing position, save a cursor returned by an earlier replay request.

Run one consumer per state file. The example does not coordinate concurrent processes or guarantee durability after a machine loses power.

<span id="4-call-the-api-without-losing-your-place" />

## 3. Handle API failures

The helper retries temporary responses and connection failures, leaving the checkpoint unchanged. Permanent errors stop the process with the API's message.

```python theme={null}
class PermanentError(RuntimeError):
    pass


class TransientError(RuntimeError):
    pass


RETRYABLE = {408, 429, 502, 503, 504}


def api_get(path, **params):
    for attempt in range(6):
        try:
            response = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=35)
        except requests.RequestException as error:
            detail = f"transport: {error}"
            if attempt < 5:
                time.sleep(2**attempt)
            continue
        if response.ok:
            return response.json()
        error = response.json()["error"]
        detail = f'{response.status_code} {error["code"]} {error.get("reason") or ""} {error["message"]}'
        if response.status_code not in RETRYABLE or error.get("reason") == "monthly_quota_exceeded":
            raise PermanentError(detail)
        if attempt < 5:
            retry_after = response.headers.get("Retry-After")
            time.sleep(int(retry_after) if retry_after else 2**attempt)
    raise TransientError(f"gave up on GET {path}: {detail}")
```

A monthly quota failure stops immediately even though its status is `429`. Read its reset time or adjust account usage on [Developers](https://0xinsider.com/developers). Do not keep a process asleep until next month.

A `400` caused by changed cursor filters also stops. Restore the previous filters, or deliberately start a new traversal without the old cursor.

<span id="6-filter-on-the-server-then-post" />

## 4. Post one alert

The consumer requests `min_grade=A`, so every returned trade has an S or A grade. Names are optional and are read with `.get()`.

```python theme={null}
def post_alert(tr):
    who = tr["trader"].get("username") or tr["trader"]["address"][:10]
    msg = (
        f'{tr["trader"]["grade"]} {who} {tr["side"]} {tr["outcome"] or ""} '
        f'${tr["size_usd"]:,.0f} @ {tr["price"]:.2f} '
        f'on {tr["market"]["title"]} (review {tr["review_score"]:.2f})'
    )
    r = requests.post(os.environ["SLACK_WEBHOOK_URL"], json={"text": msg}, timeout=30)
    if r.status_code != 200 or r.text.strip() != "ok":
        raise TransientError(f"Slack did not acknowledge the alert: HTTP {r.status_code}")
```

Slack's [incoming webhook documentation](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/) defines a successful acknowledgement as HTTP `200` with `ok`. The example checks both before allowing the checkpoint to advance.

For another destination, replace `post_alert` and require that destination's documented acknowledgement. Use an idempotency key when it supports one.

<span id="2-what-the-feed-returns" />

<span id="5-drain-the-feed-acknowledge-after-delivery" />

## 5. Process events in order

The replay request uses `min_grade=A`, `min_size=25000`, and `expand=trade`. It receives trades worth at least \$25,000 and includes their base large-trade fields in the same response.

```python theme={null}
wake = threading.Event()


def drain(state):
    """Deliver every event after the checkpoint. Returns pending_beyond_horizon."""
    while True:
        params = {"limit": 100, "min_grade": "A", "min_size": 25000, "expand": "trade"}
        if state["cursor"]:
            params["cursor"] = state["cursor"]
        page = api_get("/events/feed/since", **params)
        for ev in page["data"]:
            if ev["id"] not in state["delivered"]:
                deliver(ev)
                state["delivered"] = (state["delivered"] + [ev["id"]])[-DELIVERED_KEEP:]
            state["cursor"] = ev["cursor"]
            save_state(state)
        if not page["has_more"]:
            state["cursor"] = page["next_cursor"]
            save_state(state)
            return page["meta"]["replay"]["pending_beyond_horizon"]


def deliver(ev):
    if ev["type"] != "whale_trades_inserted":
        return
    tr = ev["trade"]
    if tr is None:
        raise TransientError(f"Trade details unavailable for {ev['id']}; checkpoint held")
    post_alert(tr)


```

The cursor advances only after `deliver` returns. A failed post or a `null` trade raises first, so the next attempt starts from the same uncompleted event.

After the last page, save `next_cursor` even if the page is empty. It can advance past rows that did not match your filters, avoiding repeated reads of those rows.

### Fields to keep straight

| Field | Meaning |
| - | - |
| Event `id` | The `ef_` event identity. Use it for deduplication. |
| Event `cursor` | The continuation point after that event. Save this, not `sequence`. |
| Event `sequence` | The raw trade ID. It can decrease when an older trade finishes writing later. |
| Event `trade` | Base large-trade fields with `expand=trade`. It can be `null` when details are unavailable; it omits detail-only counterparty analysis. |
| `next_cursor` | The continuation point after the page's examined records. |
| `has_more` | More events are available now. Request the next page immediately. |
| `meta.replay.pending_beyond_horizon` | Other recorded trades are waiting behind an unfinished write. Ask again soon. |

`trader`, `condition_id`, `min_grade`, and `min_size` are tied to the cursor. Changing them returns `400` with `cursor_expired`. Changing `expand` is allowed.

Replay cursors do not expire because of age. `meta.retention.cursor_expired` is always `false`; the error with the same name describes an incompatible cursor or filter combination.

<span id="budget" />

## 6. Run the consumer

The loop checks every 15 seconds while caught up, or every 5 seconds when the feed reports pending records. Failed delivery keeps the current checkpoint and waits 30 seconds before trying again.

```python theme={null}
state = load_state()

while True:
    try:
        pending = drain(state)
    except (TransientError, requests.RequestException) as error:
        print("holding at", state["cursor"], "because", error)
        time.sleep(30)
        continue
    wake.wait(timeout=5 if pending else 15)
    wake.clear()
```

A page contains up to 100 events and costs 1 request. Idle polling uses 4 requests per minute from the account's shared 100-request-per-minute budget. See [Rate limits](/rate-limits).

If an event keeps returning `trade: null`, inspect that event and its detail route. The example holds the checkpoint indefinitely; decide how your application records and handles an event whose details cannot be recovered before intentionally skipping it.

<span id="7-trigger-the-drain-from-a-webhook-or-the-stream" />

## 7. Add a webhook or stream wake-up

Both mechanisms notify you of new trade records. Keep the replay loop as the consumer and call `wake.set()` to start its next read sooner.

### Webhook

Register a destination for `whale_trades_inserted` and verify it as described in [Webhooks](/guides/webhooks). Verify the request signature before passing the event to this handler sketch:

```python theme={null}
def handle_event(event):
    # Call this after the signature check passed.
    if event["type"] == "whale_trades_inserted":
        wake.set()
```

Answer within the delivery's 15-second timeout. Run the replay work outside the HTTP handler. The event contains a count, not the trade object.

### SSE stream

Request [Stream](/api-reference/endpoint/get-stream) with `event=WhaleTradesInserted`:

```bash theme={null}
curl -N -H "Authorization: Bearer $OXINSIDER_API_KEY" \
  "https://api.0xinsider.com/api/v1/stream?event=WhaleTradesInserted"
```

Use a stream reader in your application to call `wake.set()` when a frame arrives. Each frame has an `id` and a JSON body containing `seq`, `published_at`, `type`, and `count`.

Send `Last-Event-ID` when reconnecting. A `resync` means retained stream history cannot cover that gap; the replay loop still resumes from its durable cursor.

Do not add `condition_id` or `min_grade` to this stream subscription. These count-only frames lack those fields and would all be filtered out. An open-stream cap can return `429`; honor `Retry-After` before reconnecting.

<span id="coverage-and-delivery" />

<span id="what-this-does-not-do" />

<span id="related" />

## Delivery and coverage limits

* Alerts describe already executed trades. `traded_at` is the fill time, and replay begins when 0xinsider records the trade.
* Coverage includes recorded large trades, not every Polymarket fill.
* The current grade, username, and `review_score` can change before you read an event. `recorded_review_score` and `trader.grade_at_trade` record trade-time values when available.
* A crash between delivery and checkpoint storage can produce a duplicate. Exactly-once posting requires support from the destination.
* An unavailable trade or broken destination holds the checkpoint until it is resolved or handled explicitly.
* Webhook deliveries can reach `dead_letter` after retries. The replay loop is independent; use [Redeliver a webhook delivery](/api-reference/endpoint/redeliver-webhook-delivery) if the webhook itself must be retried.
* The API does not place orders.

Use [Copy-trade signals](/recipes/copy-trade) to match trades against a wallet list, or [Review suspicious trades](/recipes/insider-scanner) to investigate flags.


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