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

# How to Build a BTC 5-Minute and 15-Minute Bot on Polymarket

> Build a Polymarket bot for BTC 5-minute and 15-minute up/down markets: Chainlink TWAP settlement, deterministic market discovery, the fee math that decides your edge, and execution through the Bravado API.

## Overview

Polymarket runs a rolling series of short-dated crypto markets. Every five minutes a new **Bitcoin Up or Down** market opens a window, and every fifteen minutes a longer one does the same. Each asks one question: at the end of this window, is BTC higher than it was at the start?

On **7 August 2026** the way these markets settle changed. They no longer read a single price at the instant the window closes. They read a **Chainlink time-weighted average price** over the final seconds of the window. That change is the whole reason this guide exists, because it moves the decision point of the market from one instant to a measurable interval, and a bot written against the old behaviour is now systematically wrong at exactly the moment it matters.

This guide covers what settles these markets, how to find and price them, what the fees do to your edge, and how to execute through the Bravado Trade API.

<Card title="Spin up a Trade API key" icon="key" href="https://portal.bravadotrade.com/dashboard">
  Create a key with `trade.read`, `trade.execute`, and `trade.cancel` in the **Bravado Portal**. Every example below runs in dry mode without one, and needs one the moment you go live.
</Card>

<Note>
  Read time: about 20 minutes. Python and REST experience assumed. Read [Build a trading bot](/guides/build-a-trading-bot) first for the state and reconciliation patterns this guide leans on.
</Note>

## TL;DR

* **It is Chainlink, not Pyth.** Crypto up/down markets resolve against Chainlink Data Streams TWAP feeds. Pyth is Polymarket's resolution source for equities, ETFs, and commodities, not for BTC.
* **5-minute markets use a 30-second TWAP lookback. 15-minute markets use 60 seconds.** Read the window from the market's `cryptoMarketConfig` rather than hardcoding it.
* **Market slugs are deterministic.** `btc-updown-5m-<unix window start>`. You never need to search for the next market, you can compute it.
* **Taker fees dominate the edge.** At 50 cents a taker pays 1.75 cents per share. You need roughly 1.75 points of probability edge just to break even. Rest as a maker whenever you can.
* **Bravado is the execution layer, Polymarket is the data layer.** There is no order book endpoint on Bravado, and you do not want one in the hot loop.
* **Advanced order types mostly do not apply here.** `PEGGED` is the one worth testing. `TWAP`, `ICEBERG`, and the stop variants are built for hours, not for a 300-second window.
* **Location is a first-class design decision.** Polymarket's primary servers are in `eu-west-2`.

## What you will do

* Resolve the current and next BTC window without searching
* Read the TWAP configuration off the market instead of assuming it
* Stream Chainlink TWAP prices from Polymarket RTDS
* Compute a fair probability and compare it to a live book
* Size an order against the fee, not against your conviction
* Execute through Bravado with retry-safe keys
* Decide where to run the process

## What you will need

**Knowledge**

* Python 3.11 or later, comfort with REST and WebSockets
* A directional view on BTC over a 5 to 15 minute horizon. This guide gives you the harness, not the signal.

**Tools and access**

* A Bravado API key with `trade.read`, `trade.execute`, and `trade.cancel`, created in the [Bravado Portal](https://portal.bravadotrade.com/dashboard)
* `pip install requests websockets`
* pUSD collateral for live trading. Dry-run mode needs none.
* A machine with a clock disciplined by NTP. Window boundaries are absolute times and drift will cost you.

```bash theme={null}
export BRAVADO_API_KEY="your-bearer-token"
export BASE="https://bravado-api-k7kaq.ondigitalocean.app"
```

## What settles these markets

Each crypto up/down market carries its rules in its own description. This is the current text on a BTC 5-minute market, verbatim:

> This market will resolve to "Up" if the time-weighted average price (TWAP) of Bitcoin, generated by Chainlink, of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to "Down".

The resolution source is a Chainlink Data Streams feed, published on the market as a URL:

| Duration   | Series slug             | TWAP lookback | Resolution source                                  |
| ---------- | ----------------------- | ------------- | -------------------------------------------------- |
| 5 minutes  | `btc-up-or-down-5m`     | 30 seconds    | `data.chain.link/streams/btc-usd-twap-30s-streams` |
| 15 minutes | `btc-up-or-down-15m`    | 60 seconds    | `data.chain.link/streams/btc-usd-twap-60s-streams` |
| 4 hours    | `btc-up-or-down-4h`     | 60 seconds    | `data.chain.link/streams/btc-usd-twap-60s-streams` |
| Hourly     | `btc-up-or-down-hourly` | None          | Binance BTC/USDT                                   |

<Warning>
  The hourly series is a different animal. It still resolves against a centralised exchange price and carries no `cryptoMarketConfig`. Do not assume a bot written for 5m and 15m transfers to it.
</Warning>

### It is Chainlink, not Pyth

Worth stating plainly because the assumption is common. Polymarket uses two price oracles for different asset classes:

| Asset class                                               | Oracle                     | Where it shows up                                        |
| --------------------------------------------------------- | -------------------------- | -------------------------------------------------------- |
| Crypto up/down (BTC, ETH, SOL, XRP, DOGE, BNB, HYPE, ZEC) | **Chainlink Data Streams** | The TWAP feeds this guide uses                           |
| Equities, ETFs, forex, commodities                        | **Pyth Network**           | The `equity_prices` RTDS topic                           |
| Everything else                                           | **UMA Optimistic Oracle**  | See [UMA resolution](/markets/polymarket/uma-resolution) |

If you are building for BTC, Pyth is not in your path.

### Read the window, do not hardcode it

Every crypto up/down market exposes its settlement configuration as a structured field. Read it:

```json theme={null}
{
  "cryptoMarketConfig": {
    "id": "btc-5m-twap-30",
    "asset": "btc",
    "duration": "5m",
    "twapEnabled": true,
    "twapLookbackSeconds": 30
  }
}
```

`twapLookbackSeconds` is the number your bot cares about. It is 30 on 5-minute markets and 60 on 15-minute markets today, and it is the kind of parameter that moved once already this year. A bot that reads it survives the next change. A bot that hardcodes `30` silently mis-prices every market the day it moves.

<Note>
  Chainlink does not publish the sampling boundaries, weighting, or rounding behaviour of these custom feeds. You cannot reproduce the settlement value independently, and you should not try. Consume the published feed and treat it as authoritative.
</Note>

### The reference price is the part to verify yourself

The rule compares the TWAP at the end of the window against "the price at the beginning of that range". Polymarket does not publish a separate reference-price endpoint, and the TWAP stream has no history or replay after a disconnect.

The practical consequence: **your bot has to snapshot the reference itself, at the window open, and keep it.** Before you risk capital, run the harness in dry mode for a few hundred windows, log your snapshot and the settled outcome side by side, and confirm your reference reconstruction agrees with how the market actually resolved. This is not optional diligence. It is the one part of the model you cannot verify from documentation alone.

## Find the market without searching

The single most useful property of these series: **the slug is a pure function of the window start time.**

```
btc-updown-5m-<unix timestamp of window start, UTC>
btc-updown-15m-<unix timestamp of window start, UTC>
```

So the market whose window opens at the next five-minute boundary is:

```python theme={null}
import time

def slug_for(duration_sec: int, window_start: int) -> str:
    tag = "5m" if duration_sec == 300 else "15m"
    return f"btc-updown-{tag}-{window_start}"

now = int(time.time())
current_5m = now - now % 300          # window already running
next_5m    = current_5m + 300         # window that opens next
```

Resolve it in one call:

```bash theme={null}
curl -s "https://gamma-api.polymarket.com/markets?slug=btc-updown-5m-1786331400"
```

<Warning>
  **Do not discover these markets by listing and sorting.** Polymarket creates each window roughly 24 hours ahead of time, so a query ordered by `startDate` descending returns markets whose windows open tomorrow, not the one about to run. That is the most expensive discovery bug in this series: everything looks correct, the book is liquid, and you are trading a window a day away.
</Warning>

### What a real window looks like

Three consecutive BTC 5-minute books, sampled two seconds before the top of a window:

| Window                   | Time to open | Best bid / ask on Up | Reading                                   |
| ------------------------ | ------------ | -------------------- | ----------------------------------------- |
| Running now, 2s to close | Closing      | `0.99` bid, no ask   | Outcome already known, book has collapsed |
| Opens in 2s              | 0s           | `0.54 / 0.55`        | Market already leaning up                 |
| Opens in 5m 2s           | 300s         | `0.50 / 0.51`        | A coin flip with no information yet       |

That progression is the shape of the entire strategy space. A window is a coin flip when it opens, becomes tradeable as the underlying moves, and is fully priced before it closes. Your bot lives in the middle.

<Note>
  The `bestBid` and `bestAsk` fields on the Gamma market object are cached and were stale in every sample above. For anything you trade on, read `https://clob.polymarket.com/book?token_id=...`.
</Note>

### Venue parameters

| Parameter             | 5-minute                     | 15-minute                   |
| --------------------- | ---------------------------- | --------------------------- |
| Tick size             | `0.01`                       | `0.01`                      |
| Minimum resting order | 5 shares                     | 5 shares                    |
| Minimum market order  | \$1 notional                 | \$1 notional                |
| Outcomes              | `Up` / `Down`, two token ids | Same                        |
| Neg risk              | No                           | No                          |
| Order book opens      | About 24h before the window  | About 24h before the window |

A one cent tick on a market that lives for 300 seconds is coarse. It means the smallest expressible edge is one full percentage point of probability, which matters a great deal once you see the fee.

## The fee decides the strategy

Polymarket charges a taker fee on crypto markets, and it is not small:

```
fee_per_share = 0.07 × p × (1 − p)
```

Where `p` is the share price. Makers pay nothing and receive a share of collected fees as a rebate. Bravado charges 10 bips of notional per side on top.

The fee peaks exactly where these markets live:

| Price  | Taker fee per share | As a share of notional | Probability edge needed to break even |
| ------ | ------------------- | ---------------------- | ------------------------------------- |
| `0.50` | 1.75¢               | 3.50%                  | 1.75 points                           |
| `0.60` | 1.68¢               | 2.80%                  | 1.68 points                           |
| `0.70` | 1.47¢               | 2.10%                  | 1.47 points                           |
| `0.90` | 0.63¢               | 0.70%                  | 0.63 points                           |

Read the last column carefully. To cross the spread at 50 cents and break even, your estimate of the true probability has to beat the offered price by 1.75 percentage points. The tick is one cent. So a taker strategy at the money needs close to two ticks of genuine edge on every trade, before slippage, before any error in your reference price, and before the 10 bips Bravado adds.

<Warning>
  This is the number that kills naive bots in this series. A signal that is right 52% of the time is a losing strategy as a taker at 50 cents, and a comfortably profitable one as a maker. Design for the book, not against it.
</Warning>

**The consequence for design:** rest limit orders and get paid the spread. Take only when the underlying has moved far enough that your fair value has left the current price by more than the fee. In practice that means late in the window, in the direction the TWAP is already going.

## Architecture: Polymarket for data, Bravado for execution

Bravado has no order book, midpoint, or market discovery endpoint, and that is the correct split for this workload. Price data belongs on a WebSocket you own; execution belongs behind an API that handles signing, idempotency, and strategy supervision.

<Steps>
  <Step title="Stream the TWAP">
    Polymarket RTDS pushes Chainlink TWAP updates about once a second, with no credentials. This is your signal input.
  </Step>

  <Step title="Resolve the window">
    Compute the slug, fetch the market once per window, cache the token ids and `twapLookbackSeconds`.
  </Step>

  <Step title="Price it">
    Reference price plus current TWAP plus time remaining gives a fair probability.
  </Step>

  <Step title="Execute through Bravado">
    `POST /v2/trade/order` with an idempotency key. Rest as a maker by default.
  </Step>

  <Step title="Flatten or let it settle">
    Cancel anything unfilled before the window closes. Redeem the winners.
  </Step>
</Steps>

### Streaming the TWAP

RTDS is live and is the recommended path. No API key, no Chainlink credentials, no report decoding.

```python theme={null}
import asyncio, json, websockets
from decimal import Decimal

RTDS = "wss://ws-live-data.polymarket.com"

SUB = {
    "action": "subscribe",
    "subscriptions": [
        {"topic": "crypto_prices_twap_thirty", "type": "update",
         "filters": '{"symbol":"btc/usd"}'},
        {"topic": "crypto_prices_twap_sixty", "type": "update",
         "filters": '{"symbol":"btc/usd"}'},
    ],
}

E18 = Decimal(10) ** 18


async def stream_twap(state: dict):
    async for ws in websockets.connect(RTDS, ping_interval=None):
        try:
            await ws.send(json.dumps(SUB))
            asyncio.create_task(heartbeat(ws))
            async for raw in ws:
                msg = json.loads(raw)
                p = msg.get("payload") or {}
                if "full_accuracy_value" not in p:
                    continue
                window = int(p["window_s"])
                state[window] = {
                    # exact E18 fixed point, never the float in `value`
                    "price": Decimal(p["full_accuracy_value"]) / E18,
                    "observed_at": int(p["timestamp"]) // 1000,
                }
        except websockets.ConnectionClosed:
            continue          # reconnect and resubscribe


async def heartbeat(ws):
    while True:
        await asyncio.sleep(5)
        await ws.send("PING")
```

Three things in there are load-bearing:

* **`full_accuracy_value` is the price.** It is a signed E18 fixed-point integer. The `value` field beside it is a float provided for display, and it is not what settles the market. Divide the integer with `Decimal`.
* **`payload.timestamp` is the Chainlink observation time.** The outer `timestamp` is when Polymarket relayed it. In sampling, the gap between them ran about 1 to 2 seconds. Use the inner one for freshness checks, and budget for the relay lag in your model.
* **`PING` every 5 seconds, as a text frame.** RTDS uses an application-level heartbeat and will drop you without it.

<Note>
  Subscriptions start with the next update. There is no snapshot, no history, and no replay after a disconnect. If your process restarts mid-window, you have lost the reference price for that window. Treat that as a hard skip, not a guess.
</Note>

<Accordion title="When to use Chainlink Data Streams directly instead">
  Go direct to `wss://ws.dataengine.chain.link` when you need the original signed report, want to verify DON signatures before acting, or want to remove Polymarket's relay from your latency path. It costs you: Data Streams credentials, request signing, a server clock within five seconds of Chainlink's, and report decoding via `@chainlink/data-streams-sdk`. Feed IDs come from the [Data Streams catalog](https://data.chain.link/streams) under the `TWAP: 30s` and `TWAP: 60s` tickers. For most bots the 1 to 2 seconds of relay lag is not the binding constraint, and RTDS is the better trade.
</Accordion>

### Resolving the window

```python theme={null}
import json, requests
from decimal import Decimal

GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"


def load_window(duration_sec: int, window_start: int) -> dict | None:
    tag = "5m" if duration_sec == 300 else "15m"
    r = requests.get(f"{GAMMA}/markets",
                     params={"slug": f"btc-updown-{tag}-{window_start}"},
                     timeout=5)
    r.raise_for_status()
    rows = r.json()
    if not rows:
        return None

    m = rows[0]
    cfg = m.get("cryptoMarketConfig") or {}
    if not cfg.get("twapEnabled"):
        return None                      # pre-TWAP or unmigrated series

    up, down = json.loads(m["clobTokenIds"])
    return {
        "slug": m["slug"],
        "condition_id": m["conditionId"],
        "up": up,
        "down": down,
        "lookback": int(cfg["twapLookbackSeconds"]),
        "opens_at": window_start,
        "closes_at": window_start + duration_sec,
        "tick": Decimal(str(m["orderPriceMinTickSize"])),
        "min_size": Decimal(str(m["orderMinSize"])),
        "accepting": bool(m.get("acceptingOrders")),
    }


def book(token_id: str) -> tuple[Decimal | None, Decimal | None]:
    b = requests.get(f"{CLOB}/book", params={"token_id": token_id}, timeout=5).json()
    bids = sorted((Decimal(x["price"]) for x in b.get("bids", [])), reverse=True)
    asks = sorted(Decimal(x["price"]) for x in b.get("asks", []))
    return (bids[0] if bids else None, asks[0] if asks else None)
```

Note the `twapEnabled` guard. It is the cheapest possible protection against pointing a TWAP-aware bot at a series that does not settle on TWAP.

### Pricing the window

The fair probability of `Up` is the probability that the closing TWAP finishes at or above your reference. Everything you know reduces to three quantities: how far the TWAP has already moved, how much time is left, and how volatile BTC is over that horizon.

```python theme={null}
from decimal import Decimal
from statistics import NormalDist


def fair_up_probability(reference: Decimal, current: Decimal,
                        seconds_left: int, sigma_per_sqrt_sec: Decimal) -> Decimal:
    """Drift-free diffusion estimate. Replace with your own model."""
    if seconds_left <= 0:
        return Decimal(1) if current >= reference else Decimal(0)

    move = (current - reference) / reference
    scale = sigma_per_sqrt_sec * Decimal(seconds_left).sqrt()
    if scale == 0:
        return Decimal(1) if current >= reference else Decimal(0)

    return Decimal(NormalDist().cdf(float(move / scale)))
```

This is a placeholder and it is deliberately naive. It assumes no drift, constant volatility, and that the settlement value equals the last TWAP print. Two effects it ignores are worth naming, because they are where the real work is:

* **The lookback window flattens the endgame.** With 30 seconds of lookback on a 5-minute market, roughly the final 10% of the window is already being averaged in. A move in the last 5 seconds moves the settlement value by about a sixth of its magnitude. Under the old snapshot rule that same move counted in full. Any model carried over from before 7 August 2026 systematically overestimates how much late price action matters.
* **Your view is delayed.** The relay adds 1 to 2 seconds and the feed prints about once a second. With 8 seconds left on the clock, you are pricing off information that is a meaningful fraction of the remaining window old.

### Executing through Bravado

One endpoint, one idempotency key per intended order. If you have not created an API key yet, do it now in the [Bravado Portal](https://portal.bravadotrade.com/dashboard) and grant `trade.read`, `trade.execute`, and `trade.cancel`, since the flatten step below needs all three.

```python theme={null}
import os, uuid, requests
from decimal import Decimal

BASE = os.environ["BASE"]
H = {"Authorization": f"Bearer {os.environ['BRAVADO_API_KEY']}"}

DRY_RUN = True
TAKER_RATE = Decimal("0.07")
BRAVADO_BIPS = Decimal("0.001")


def taker_fee_per_share(price: Decimal) -> Decimal:
    return TAKER_RATE * price * (1 - price)


def place(payload: dict, key: str) -> dict:
    if DRY_RUN:
        print(f"  DRY RUN  {payload}")
        return {"dry_run": True}
    r = requests.post(f"{BASE}/v2/trade/order",
                      headers={**H, "Idempotency-Key": key},
                      json=payload, timeout=10)
    r.raise_for_status()
    return r.json()


def trade_window(win: dict, fair: Decimal, size: Decimal):
    token = win["up"]
    bid, ask = book(token)
    if bid is None or ask is None:
        return

    # Taking: only when the edge clears the fee outright.
    if ask is not None:
        edge = fair - ask - taker_fee_per_share(ask) - ask * BRAVADO_BIPS
        if edge > 0:
            key = str(uuid.uuid4())
            return place({
                "type": "MARKET",
                "symbol": token,
                "side": "BUY",
                "quote_amount": str((size * ask).quantize(Decimal("0.01"))),
                "time_in_force": "IOC",
            }, key)

    # Otherwise rest inside the spread and get paid to wait.
    quote = min(fair - win["tick"], bid + win["tick"])
    quote = quote.quantize(win["tick"])
    if quote <= 0 or size < win["min_size"]:
        return

    key = str(uuid.uuid4())
    return place({
        "type": "LIMIT",
        "symbol": token,
        "side": "BUY",
        "price": str(quote),
        "size": str(size),
        "time_in_force": "GTC",
    }, key)
```

<Warning>
  Generate the idempotency key **outside** any retry loop. Inside it, every attempt carries a fresh key and every one that lands creates its own position. In a series that opens a new market every five minutes, a duplicate is not a one-off, it compounds 288 times a day. See [Safe retries](/guides/safe-retries-idempotency).
</Warning>

### Closing out the window

Two calls, and they are not interchangeable:

```python theme={null}
def flatten(win: dict):
    # CLOB orders: LIMIT, MARKET, TAKE_PROFIT
    requests.delete(f"{BASE}/v2/trade/orders", headers=H, timeout=10)

    # Managed strategies: PEGGED, TWAP, ICEBERG, STOP_LOSS, TRAILING_STOP
    strategies = requests.get(f"{BASE}/v2/trade/strategies",
                              headers=H, timeout=10).json()["strategies"]
    for s in strategies:
        if s["status"] in ("ACTIVE", "PENDING") and s["symbol"] in (win["up"], win["down"]):
            requests.delete(f"{BASE}/v2/trade/strategies/{s['record_id']}",
                            headers=H, timeout=10)
```

Cancel-all clears CLOB orders only. A `PEGGED` order left running past the close belongs to a market that no longer exists in any useful sense.

After settlement, winning shares are redeemable but **not redeemed automatically**. Call `POST /v2/trade/positions/redeem` once `position.redeemable` is `true`. Running 288 windows a day, unredeemed winners are the most common form of capital sitting idle in this strategy.

## Confirming what you do not need

You were right to be suspicious of both.

### Advanced order types

| Type                                        | Verdict on a 5m or 15m window                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `LIMIT`                                     | **Yes.** Your default. Rests, earns the spread, pays no Polymarket fee.                                                                                                                                                                                                                                                                          |
| `MARKET`                                    | **Yes, selectively.** Only when the edge clears the taker fee. `IOC` so it never rests by accident.                                                                                                                                                                                                                                              |
| `PEGGED`                                    | **Worth testing.** Chases the touch within `price_floor` and `price_ceiling` on a budget, which is exactly what a maker in a 300-second window wants. It is a managed strategy, so it returns a `record_id` and cancels via `/v2/trade/strategies`. Test it on 15-minute windows first, where its lifetime is long enough to be worth the setup. |
| `TWAP`                                      | **No.** Minimum `interval_sec` is 10, so a 300-second window buys you at most 30 clips of a position you want on now, not averaged in. It also leaks your direction into a book with a few hundred shares on the touch.                                                                                                                          |
| `ICEBERG`                                   | **No.** Post-only slices designed to hide size. There is no size to hide here.                                                                                                                                                                                                                                                                   |
| `STOP_LOSS`, `TAKE_PROFIT`, `TRAILING_STOP` | **No.** These protect positions held across hours. A position here resolves in minutes to \$1 or \$0, and a stop that triggers with 40 seconds left crosses a spread to exit a bet that was about to settle anyway.                                                                                                                              |

The general rule: Bravado's advanced types exist to work size into thin books over time. These markets are the opposite problem, small size and no time.

### The Data API

Not in the hot loop. It reads on-chain Polygon settlement records, so it is the wrong latency class for a decision you make every second.

It is the right tool the moment the window closes. Run 288 windows a day and the only question that matters is whether the strategy is profitable **after fees**, which is precisely what a cashflow-model PnL over your own wallet tells you. Pull `GET /traders/{address}/pnl` and `GET /traders/{address}/trades` on a daily cadence and check the answer against your own logs. See [Build a PnL leaderboard](/guides/pnl-leaderboard).

## Latency and location

### Where the venue is

Polymarket publishes this, and it changes how you should think about hosting:

|                                  | Region                                               |
| -------------------------------- | ---------------------------------------------------- |
| Primary servers                  | `eu-west-2` (London)                                 |
| Closest non-georestricted region | `eu-west-1` (Ireland)                                |
| Direct co-location               | Available after completing Polymarket's KYC/KYB form |

Typical inter-region round trips, as orders of magnitude. Measure your own rather than trusting the table:

| Where your bot runs               | Round trip to `eu-west-2`             |
| --------------------------------- | ------------------------------------- |
| `eu-west-2`, co-located           | Around 1 ms                           |
| `eu-west-1`                       | Around 10 ms                          |
| `us-east-1`                       | Around 75 ms                          |
| `us-west-2`                       | Around 135 ms                         |
| `ap-southeast-1`                  | Around 170 ms                         |
| A laptop on residential broadband | 100 ms to 300 ms, and highly variable |

### What that actually costs you

Be honest about the mechanism, because latency matters here for a narrower reason than people assume.

The TWAP feed prints about once a second and the relay adds 1 to 2 seconds. So 50 milliseconds of network time does not change what you know. What it changes is **what you can do about it**:

* **Queue position.** A one cent tick means many participants want the same price level. On a book with a few hundred shares at the touch, arriving first at a level is most of the game.
* **Cancel races.** When the TWAP prints and the fair value moves, your resting order is now mispriced and someone else's taker is on the way to hit it. The gap between your cancel and their take is measured in exactly the milliseconds this table describes.
* **The final window.** In the last 30 or 60 seconds the outcome is being averaged into existence. The book repriced from `0.54 / 0.55` to `0.99` bid, no ask inside a single window in the sample above. Being 150 milliseconds late into that transition is the difference between a fill and a chase.

### Why execute through Bravado

Two concrete reasons, plus one thing to check for yourself.

**You skip order construction entirely.** A native CLOB order requires building and EIP-712 signing the order locally before it can be sent. Through Bravado it is one authenticated JSON POST. That removes local signing work from the path between your decision and the venue, on every order, in a loop that runs 288 times a day.

**Managed strategies run server-side.** If you use `PEGGED`, its chase logic lives on Bravado's infrastructure, adjacent to the venue, rather than in a loop on your box that has to observe a move, decide, and send. Your process restarting does not abandon it mid-window.

**Then measure the hop.** Routing through Bravado inserts a network hop between you and Polymarket. That is a clear win when Bravado sits closer to `eu-west-2` than your process does, which is the common case for anyone not already co-located, and it is worth confirming against your own numbers before you scale size. Time a round trip through `POST /v2/trade/order` in dry conditions and compare it against your own path to the CLOB. Optimise the leg that is actually costing you.

<Note>
  The single largest latency lever available to most people is not the API they use. It is moving the process out of a home connection or a US region and into Europe. Do that first, then tune.
</Note>

## Eligibility, before you write any of this

Polymarket restricts order placement by jurisdiction, and the restrictions apply to the **API**, not just the website.

```bash theme={null}
curl -s https://polymarket.com/api/geoblock
```

```json theme={null}
{ "blocked": false, "ip": "203.0.113.4", "country": "US", "region": "NY" }
```

* **Blocked entirely:** Iran, Syria, Cuba, North Korea, and the Crimea, Donetsk, and Luhansk regions of Ukraine. No new orders, no closing existing positions.
* **Close-only on frontend and API:** a longer list that includes the **United States**, the United Kingdom, Canada, Australia, France, Germany, Belgium, Brazil, and Russia. Existing positions can be closed. New positions cannot be opened.
* **Close-only on frontend, API unrestricted:** Ireland, Japan, Malta, the Netherlands.

<Warning>
  Hosting a process in `eu-west-2` is a latency decision. It is not a compliance decision and does not change your eligibility. Confirm your own status against the geoblock endpoint and against your Bravado account terms before you build. Do not design around these restrictions.
</Warning>

## Going live

<Steps>
  <Step title="Run dry for 200 windows">
    `DRY_RUN = True`. Log your reference snapshot, your fair value, the book, and the settled outcome for every window. This is how you validate the reference-price reconstruction, which is the one thing documentation cannot confirm for you.
  </Step>

  <Step title="Check the calibration, not the hit rate">
    Bucket your fair values and compare each bucket against realised outcomes. A model that says 60% should be right about 60% of the time. A high hit rate with poor calibration will not survive the fee.
  </Step>

  <Step title="Trade one series, maker only">
    Turn off the taker branch entirely. Run 15-minute windows first: the same mechanics with four times the decision time and less exposure to relay lag.
  </Step>

  <Step title="Reconcile against the Data API">
    After a week, compare your logged PnL against `GET /traders/{address}/pnl`. If they disagree, your fee accounting is wrong, and fee accounting is the whole margin here.
  </Step>

  <Step title="Then add 5-minute windows, then add taking">
    In that order. Each step adds a distinct failure mode and you want to know which one broke.
  </Step>
</Steps>

## Wrapping up

The TWAP change made these markets harder to manipulate and easier to model. Settlement is now an average over a defined interval published on a public feed you can subscribe to for free, rather than a single number read at an instant you had to race.

What is left is an execution problem with an unusually explicit cost. You know the fee formula, you know the tick, you know the feed cadence, and you know where the venue is. The 1.75 cents per share at the money is the number the strategy lives or dies on, and almost everything in this guide is downstream of the decision to rest rather than cross.

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Is Pyth involved anywhere in BTC up/down markets?">
    No. Crypto up/down markets resolve against Chainlink Data Streams TWAP feeds. Pyth is Polymarket's resolution source for equities, ETFs, forex, and commodities, and it surfaces on the `equity_prices` RTDS topic. If your market is a BTC window, Pyth is not in the path.
  </Accordion>

  <Accordion title="Why is my market showing a book for a window that is a day away?">
    Because Polymarket creates each window roughly 24 hours ahead of time. If you discovered the market by listing events sorted by `startDate` descending, you found tomorrow's window rather than the current one. Compute the slug from the window start timestamp instead.
  </Accordion>

  <Accordion title="Can I reproduce the settlement TWAP myself and front-run it?">
    No. Chainlink does not publish the sampling boundaries, weighting, rounding, or missing-input behaviour of these custom feeds, so an independently computed value will not reliably match. Consume the published feed.
  </Accordion>

  <Accordion title="My bot restarted mid-window. Can it recover the reference price?">
    Not from the feed. RTDS subscriptions start with the next update and there is no snapshot, history, or replay. Persist your reference snapshot as soon as you take it, and if it is missing for a window, skip that window rather than estimating it.
  </Accordion>

  <Accordion title="Why is my 52%-accurate signal losing money?">
    Almost certainly the taker fee. At 50 cents a taker pays 1.75 cents per share, which is 3.5% of notional and about 1.75 points of probability. A 52% signal has 2 points of edge before costs and close to nothing after them. Rest as a maker and the same signal has a very different economics.
  </Accordion>

  <Accordion title="Should I use a TWAP order to enter a TWAP-settled market?">
    No, and the name collision is a coincidence worth naming out loud. Bravado's `TWAP` order type spreads your execution over time to reduce market impact. The market's TWAP settlement is how the outcome is computed. In a 300-second window with a minimum `interval_sec` of 10, a TWAP order gives you a slowly-accumulated position in a market that will have resolved before it finishes.
  </Accordion>

  <Accordion title="Do I need to redeem winners manually?">
    Yes. Redemption is not automatic. Check `position.redeemable` and call `POST /v2/trade/positions/redeem`. At 288 windows a day this adds up quickly if you skip it.
  </Accordion>

  <Accordion title="Does the 15-minute series behave the same as the 5-minute one?">
    Same mechanics, different constants. The lookback is 60 seconds rather than 30, so the final minute is averaged rather than the final half-minute, and you have four times as long to act on the same information. Start there.
  </Accordion>
</AccordionGroup>

## Resources

* [Place an order](/guides/place-an-order), every order type in detail
* [Build a trading bot](/guides/build-a-trading-bot), reconciliation and restart safety
* [Safe retries](/guides/safe-retries-idempotency), idempotency keys in depth
* [Trade API reference](/products/trade-api), fields, fees, and position mechanics
* [Polymarket coverage](/markets/polymarket/overview), venue constraints and supported products
* [Bravado Portal](https://portal.bravadotrade.com/dashboard), create and manage API keys and scopes
* [Chainlink TWAP prices](https://docs.polymarket.com/market-data/chainlink-twap), the settlement feed
* [Geographic restrictions](https://docs.polymarket.com/api-reference/geoblock), eligibility and server regions
