vike

Example 17 of 32

Polymarket arbitrage bot: catch Up + Down under $1

Poll both tokens' books with /orderbook, add the two best asks only when both snapshots carry the same as_of_ts, and walk both ladders against the taker fee; one snapshot always prices the pair at $1 plus the spread, so the sums under $1 a polling loop sees come from snapshots seconds apart.

One Up share and one Down share pay exactly $1 at resolution, so a bot that buys both for less, fees included, locks in the difference. GET /v1/polymarket/orderbook answers each half of the check in one call: the recorded book of one token, up to 25 levels a side, with as_of_ts, the venue time of the snapshot. The loop polls both tokens, adds the two best asks, walks both ladders to count the pairs that fit under $1 and logs what it finds. Do Up and Down shares ever cost less than $1 together on 5-minute BTC markets? measures the same gap over a whole recorded day, to the millisecond; this is the live loop, and what is left of the edge after depth and fees. The full parameter list is on Order-book snapshot.

Quick answer

One poll is the Up token's whole book.

cURL
# the Up token's book in the 5-minute BTC market of 17:05 UTC, 25 levels a side
curl -H "X-API-Key: $VIKE_API_KEY" \
  "https://data.vike.io/v1/polymarket/orderbook?token_id=71017960874711395588755286498180410277662891295563354128010828164009876221919&depth=25"

Example response

Captured from the live API on 2026-10-25 at 17:09 UTC, 40 seconds before the window closed, trimmed to the best 3 of 9 asks and the best 3 of 25 bids. Up asked 0.91 for 208.75 shares and bid 0.90 for 220.19, in a snapshot taken 2 seconds before the response. Asks come first, then bids, each from the touch outward.

Example response
[
  {"as_of_ts": 1792948158349, "level": 1, "price": 0.91, "side": "ask", "size": 208.75},
  {"as_of_ts": 1792948158349, "level": 2, "price": 0.92, "side": "ask", "size": 58.5},
  {"as_of_ts": 1792948158349, "level": 3, "price": 0.93, "side": "ask", "size": 338.5},
  {"as_of_ts": 1792948158349, "level": 1, "price": 0.9, "side": "bid", "size": 220.19},
  {"as_of_ts": 1792948158349, "level": 2, "price": 0.89, "side": "bid", "size": 347.98},
  {"as_of_ts": 1792948158349, "level": 3, "price": 0.88, "side": "bid", "size": 216.0}
]

Variations

The Down book of the same market. Polled 2 seconds later, it carried the same as_of_ts and was the Up book turned over: each of its 25 asks is an Up bid at one minus the price with the same size (Down 0.10 for 220.19 is Up's 0.90 bid for 220.19), and its 9 bids are the Up asks. So one snapshot prices the cheapest pair at the Up ask plus one minus the Up bid, 0.91 + 0.10 = $1.01: one dollar plus the spread.

cURL — the Down book
# the Down token's book, polled right after
curl -H "X-API-Key: $VIKE_API_KEY" \
  "https://data.vike.io/v1/polymarket/orderbook?token_id=15264935561093061531769473839831866712081495406158977870773121572402304828303&depth=25"

The market trading now. A window starts on a multiple of 300 seconds and the slug ends in that start, so the bot finds each new market by name; token_ids lists Up, then Down.

cURL — the market
# the market of the 17:05 UTC window and its two tokens
curl -H "X-API-Key: $VIKE_API_KEY" \
  "https://data.vike.io/v1/polymarket/markets?query=btc-updown-5m-1792947900&limit=1"

The loop in one script. Each tick polls both books, keeps each level once, walks the two ask ladders cheapest first while a pair plus both taker fees costs under $1, and logs the sum, the pairs and the dollars. It counts a hit only when both snapshots carry the same as_of_ts; a pair of two moments is logged as stale and skipped.

Run
pip install requests
export VIKE_API_KEY=...
python pm-updown-arb-loop.py             # real run: five polls of both books, three seconds apart
python pm-updown-arb-loop.py --dry-run   # prints the requests it would make, calls nothing
pm-updown-arb-loop.py
"""Watch the live 5-minute BTC Up/Down market for an Up ask plus a Down ask under $1 after fees, size it against both books, and log it.

Run:  VIKE_API_KEY=... python pm-updown-arb-loop.py    (--dry-run lists the requests and makes none)
"""
import os
import sys
import time
from datetime import datetime, timezone
from urllib.parse import urlencode

NAME = "pm-updown-arb-loop"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
TICKS = 5  # polls of both books per run; a real bot keeps looping
POLL_S = 3  # two requests a tick: about 30 a minute, inside the 200 a minute a key allows
# Polymarket's taker fee rate for Crypto markets on 2026-10-25 (docs.polymarket.com/trading/fees). Check it: it changes.
FEE_RATE = 0.07
SAMPLE_TOKENS = [  # --dry-run has no response to read: the Up and Down tokens of btc-updown-5m-1792947900
    "71017960874711395588755286498180410277662891295563354128010828164009876221919",
    "15264935561093061531769473839831866712081495406158977870773121572402304828303",
]


def fail(why):
    sys.exit(f"{NAME}: {why}")


def call(path, method="GET", **params):
    url = BASE + path + ("?" + urlencode(params) if params else "")
    if DRY:
        print(method, url)
        return None
    key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
    import requests

    r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
    if r.status_code != 200:
        fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
    return r.json()


def fee(price):
    """Taker fee per share, Polymarket's formula: rate * p * (1 - p). Largest at 0.50, near zero at the extremes."""
    return FEE_RATE * price * (1 - price)


def asks(book):
    """The ask ladder, best first, each level once: a snapshot can repeat its rows."""
    levels = {r["level"]: (r["price"], r["size"]) for r in book if r["side"] == "ask"}
    return [levels[k] for k in sorted(levels)]


def pairs_under_one(up, down):
    """Buy one Up and one Down at a time, cheapest asks first, while a pair plus both taker fees costs under $1.
    Returns (pairs, dollars spent)."""
    up, down = [list(x) for x in up], [list(x) for x in down]
    pairs = spent = 0.0
    while up and down:
        (pu, su), (pd, sd) = up[0], down[0]
        unit = pu + pd + fee(pu) + fee(pd)
        if unit >= 1:
            break
        q = min(su, sd)
        pairs, spent = pairs + q, spent + q * unit
        up[0][1] -= q
        down[0][1] -= q
        for ladder in (up, down):
            if ladder[0][1] <= 1e-9:
                ladder.pop(0)
    return pairs, spent


# 4 pairs at 0.50 + 0.40 plus fees; the next Down level makes the pair cost $1 before fees, so the walk stops
assert pairs_under_one([(0.50, 10)], [(0.40, 4), (0.50, 9)])[0] == 4


def main():
    start = int(time.time()) // 300 * 300  # 5-minute windows start on multiples of 300 s; the slug ends in that start
    slug = f"btc-updown-5m-{start}"
    rows = call("/v1/polymarket/markets", query=slug, limit=1)
    up_id, down_id = SAMPLE_TOKENS
    if not DRY:
        if not rows:
            fail(f"{slug} is not in the market directory")
        up_id, down_id = rows[0]["token_ids"]  # in the order of the outcomes: Up, then Down

    for _ in range(1 if DRY else TICKS):
        up_book = call("/v1/polymarket/orderbook", token_id=up_id, depth=25)
        down_book = call("/v1/polymarket/orderbook", token_id=down_id, depth=25)
        if DRY:
            return
        if not up_book or not down_book:
            fail(f"{slug}: no recorded book for one of its tokens")
        now = datetime.now(timezone.utc).strftime("%H:%M:%S")
        up_asks, down_asks = asks(up_book), asks(down_book)
        if not up_asks or not down_asks:
            print(f"{now} {slug}: one token has no asks left (the market is all but decided)")
        else:
            pairs, spent = pairs_under_one(up_asks, down_asks)
            # Two snapshots of two moments can add up to under $1 when neither moment did: act on one moment only.
            same_moment = up_book[0]["as_of_ts"] == down_book[0]["as_of_ts"]
            verdict = "no edge" if not pairs else ("HIT" if same_moment else "stale pair, skipped")
            print(f"{now} {slug}: Up {up_asks[0][0]} + Down {down_asks[0][0]} = {up_asks[0][0] + down_asks[0][0]:.2f}; "
                  f"{pairs:g} pairs under $1 after fees for ${spent:,.2f}; {verdict}")
        time.sleep(POLL_S)


if __name__ == "__main__":
    try:
        main()
    except Exception as e:  # a shape the example does not expect fails by name, not by traceback
        fail(f"{type(e).__name__}: {e}")

Before you rely on it

A sum under $1 from two polls is two moments

Each book is its own snapshot, and two polls can catch two different ones. On 2026-10-25 between 17:09 and 17:12 UTC, 26 polls of both books found both sides quoted: the 18 pairs whose snapshots shared an as_of_ts all added up to exactly 1.01, and the 8 pairs taken 0.5 to 10.1 seconds apart added up to anything from 0.95 to 1.03. All three sums under $1 were among those 8; the 0.95 one showed 694 pairs on paper that no single snapshot offered. Compare only books with the same as_of_ts, as the script does.

The fee is bigger than the gap

Polymarket's fee page on 2026-10-25 charged a taker shares × 0.07 × p × (1 − p) on Crypto markets, and a pair is two taker fills. Near 0.50 that is about 0.035 a pair: a pair at 0.62 + 0.37 = 0.99 pays 0.033 in fees and loses 0.023 a pair. The fee falls toward the extremes, 0.006 for a pair at 0.95 + 0.04. The recorded day found the median sum under $1 at 0.98, for a few dozen shares, lasting milliseconds.

A hit is as deep as the thinner touch

The walk buys one Up and one Down at a time, so the smaller of the two best asks sets the size until the next level pushes the pair to $1. In the stale pair at 17:11:32 UTC, Up offered 115.42 shares at 0.62 and Down 44.15 at 0.37: 44.15 pairs at 0.99 before fees, then Down's next level, 0.38, made the pair $1.00 and the walk stopped.

Seconds, not milliseconds

In those polls each snapshot was 0.8 to 11.8 seconds old when the answer arrived, and a real gap lasts milliseconds, so a loop on this API measures and logs; it cannot be first. There is no Polymarket WebSocket in this API: the one WebSocket, /v1/stream, pushes closed Hyperliquid candles only. Polymarket runs its own market WebSocket, and orders go through its Python SDK with your wallet, never through this API.

Ask your agent

Connected to the Vike MCP server (https://vike.io/mcp) in Claude, ChatGPT or Cursor, ask it in plain words. The polymarket_orderbook tool answers one token's book, at 1 credit a call; the loop is yours.

Ask your agent
In Polymarket's current 5-minute BTC Up or Down market, do the best Up ask and the best Down ask add up to less than $1, and are both books from the same moment?
Example response
[  {"as_of_ts": 1792948158349, "level": 1, "price": 0.91, "side": "ask", "size": 208.75},  {"as_of_ts": 1792948158349, "level": 2, "price": 0.92, "side": "ask", "size": 58.5},  {"as_of_ts": 1792948158349, "level": 3, "price": 0.93, "side": "ask", "size": 338.5},  {"as_of_ts": 1792948158349, "level": 1, "price": 0.9, "side": "bid", "size": 220.19},  {"as_of_ts": 1792948158349, "level": 2, "price": 0.89, "side": "bid", "size": 347.98},  {"as_of_ts": 1792948158349, "level": 3, "price": 0.88, "side": "bid", "size": 216.0}]