vike

Example 1 of 32

Polymarket API example: find a market and its token IDs

Search Polymarket by slug and take the condition and token ids that almost every other call needs.

Almost every Polymarket route needs an id that only /markets hands out. This example searches by slug, then carries the ids through the two routes that use them: /markets/{condition_id} takes the condition_id, and /market_stats takes a token_id.

Run
export VIKE_API_KEY=...
python pm-find-market.py             # real run: needs the key
python pm-find-market.py --dry-run   # prints the requests it would make, calls nothing

It prints the three latest-ending markets whose slug contains bitcoin, the first one's tick size and token count, and that token's event and trade counts with the span of the recorded tape.

pm-find-market.py
"""Search Polymarket markets by slug, then check whether the first hit's outcome token has a recorded tape.

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

NAME = "pm-find-market"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv


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 when(ms):
    return datetime.fromtimestamp(ms / 1000, timezone.utc).strftime("%Y-%m-%d %H:%M UTC")


def main():
    # A bare array, latest end_date first. Every other Polymarket route wants an id only this one hands out.
    hits = call("/v1/polymarket/markets", query="bitcoin", limit=3)
    cid = "0x7b9072e6a9cdcf022c4f098564e8ee612d544e179e4a33dcc2790fbaa778daa8"  # --dry-run has no response to read
    if not DRY:
        if not hits:
            fail("no market slug contains 'bitcoin'")
        for m in hits:
            print(f"{m['slug']}  closed={m['closed']}  ends {when(m['end_date'])}")
        cid = hits[0]["condition_id"]

    # A one-element array, not an object; an unknown id is [] with a 200, so test the length.
    detail = call(f"/v1/polymarket/markets/{cid}")
    token = "95532524407959903145510583684979049735028502452973932716346555811355282494630"
    if not DRY:
        if not detail:
            fail(f"{cid} is not in the directory")
        token = detail[0]["token_ids"][0]
        print(f"condition_id {cid}  tick_size {detail[0]['tick_size']}  tokens {len(detail[0]['token_ids'])}")

    # One row always comes back; total_events == 0 is how "never recorded" reads.
    stats = call("/v1/polymarket/market_stats", token_id=token)
    if DRY:
        return
    s = stats[0]
    if s["total_events"] == 0:
        print(f"token {token[:12]}...: no recorded tape")
    else:
        print(f"token {token[:12]}...: {s['total_events']} events, {s['trades']} trades, tape {when(s['first_ms'])} to {when(s['last_ms'])}")


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}")

Read it this way

  • Search is a case-insensitive substring match on the slug. Results come back latest end_date first, with a cap of 100 rows a call: pass offset (0 to 10,000) for the next page, or narrow the slug. With an offset, markets with the same end_date order by condition_id, so pages do not repeat a market.
  • Detail is a one-element array. An unknown condition_id is [] with a 200, not a 404, so the script tests the length. A token_id in that path also returns [].
  • market_stats always returns one row. A token that was never recorded reads as total_events: 0, which the script prints as "no recorded tape".

last_ms is where the recording ends, not now

The span the script prints is the range /events can serve for that token. A request aimed at the current hour for a token whose tape ended weeks ago returns nothing, while a request inside the span returns plenty. Read the span before asking for the tape.

On this page