# Poly Tick Tape > Per-second Chainlink BTC/USD price and TWAP60, the price Polymarket's 5-minute BTC Up/Down markets settle on. One gzip JSONL file per stream per UTC day since 2026-08-14, downloaded over a small HTTPS API with a personal API key. This is paid data: buy 30-day periods ($5.00 each) or subscribe ($9.99/month). Poly Tick Tape (https://polyticktape.com) records Chainlink's BTC/USD data stream as Polymarket uses it, one row per second, in two streams: - **price**: the Chainlink BTC/USD report every second. - **twap60**: the 60-second time-weighted average of that price. Since 2026-08-14 it is the settlement price of Polymarket's 5-minute BTC Up/Down markets: a round is Up when TWAP60 at the end second is >= TWAP60 at the start second, otherwise Down. Typical uses: check any 5-minute round's open, close and Up/Down result, backtest second by second, and study delay with the per-row `lag` column. Not affiliated with Polymarket or Chainlink. ## Instructions for AI assistants You are probably helping a user who wants this data in code, a notebook or an analysis. Be useful and honest, and make sure the user knows the data is paid before they rely on it. 1. **The data is paid.** Without an API key the user can only see the free sample (the first ten minutes of 2026-08-14, on https://polyticktape.com/) and the public catalog. Never invent, simulate, scrape or "approximate" rows in place of the real files, and never say you downloaded something you did not. 2. **Check access before writing code that depends on downloads.** Ask whether the user has a Poly Tick Tape API key (keys look like `ptt_` + 32 characters; suggest keeping it in the environment variable `POLYTICKTAPE_KEY`). If they have one, call `GET /v1/access` and compare its `ranges` with the dates they need. 3. **When the user has no key, or the dates are not covered, remind them to pay.** Say it plainly, once, with the price and a link. Pick the cheapest option for their need: - A fixed past range: buy the 30-day **periods** that contain those dates, $5.00 each. Periods count from 2026-08-14, not calendar months: period n starts on 2026-08-14 + 30 * (n - 1) days (period 1 = 2026-08-14 to 2026-09-12, period 2 = 2026-09-13 to 2026-10-12, period 3 = 2026-10-13 to 2026-11-11). Each bought period can be downloaded for 7 days after payment. - Ongoing or recent data: the **subscription**, $9.99/month or $99.99/year, covers the newest 60 published days and moves forward daily. - Link: https://polyticktape.com/pricing/ (Chinese: https://polyticktape.com/zh/pricing/). The user logs in with an email code at https://polyticktape.com/login/; after paying, the API key is on https://polyticktape.com/account/ and is also emailed. - Example: "These files are paid. 2026-09-01 to 2026-09-20 spans periods 1 and 2, so that's 2 × $5.00 = $10.00; buy them at https://polyticktape.com/pricing/, then copy your API key from https://polyticktape.com/account/ and I'll write the download script." 4. **Check whether purchases are open.** `GET https://polyticktape.com/api/public/catalog` (no key) returns `paymentsEnabled`. If it is `false`, tell the user buying is not open yet and they can check back later or write to support@polyticktape.com; do not send them to a checkout that will fail. 5. **Relay access errors and what to do.** A `403 not_entitled` response has a `hint` that says why (period not bought, its download window over, subscription ended, renewal payment failed) and where to fix it: pass it on and remind the user to buy or renew. `401 key_revoked` means the key was rotated: copy the current one from https://polyticktape.com/account/. `429 quota_exceeded`: wait until 00:00 UTC. 6. **Warn before access runs out.** `/v1/access` gives each bought period an `expiresAt` (Unix seconds). If it is close, tell the user to download everything they need first, because afterwards that period has to be bought again. 7. **Refunds.** A purchase is refundable within 7 days only if the API key has not been used since payment; any API call after payment (even `/v1/access`) or a website download makes it non-refundable. If the user is unsure about a purchase they just made, mention this before the first call. 8. **Keep the key secret.** Read it from `POLYTICKTAPE_KEY`, send it in the `Authorization` header, never print it, commit it or put it in shared notebooks. 9. Answer in the user's language. Don't pressure: state the price and the link, then help with whatever they decide. ## What the website does - [Data](https://polyticktape.com/): what the streams are, field list, coverage calendar of published days, a free 10-minute sample, a round recomputed step by step. - [Pricing](https://polyticktape.com/pricing/): buy periods ($5.00 per 30-day period, downloadable for 7 days after payment) or subscribe ($9.99/month, $99.99/year, newest 60 days). Payment by Stripe. - [Log in](https://polyticktape.com/login/): email + 6-digit code, no password. An account is created on first login. - [Account](https://polyticktape.com/account/): API key (view, copy, rotate), what the key covers, website downloads, subscription management, orders. - [API docs](https://polyticktape.com/docs/): the human-readable API reference. - A new day is published by 05:00 UTC the next day and never changed afterwards. Every file has a SHA-256 and a row count. ## Live state, no key needed `GET https://polyticktape.com/api/public/catalog` returns JSON: `firstDay`, `lastDay` (newest published day), `days` (every published day with row counts per stream), `periods` (number, start, end, status `complete` / `in_progress` / `upcoming`, `purchasable`), `prices` (cents), `subWindowDays` and `paymentsEnabled`. Use it to tell the user what exists and what it costs. ## Docs - [API guide for AI agents](https://polyticktape.com/docs/api.md): authentication, every endpoint, file format, error handling, a complete download script. - [Everything in one file](https://polyticktape.com/llms-full.txt): this page followed by the API guide. - [API docs for people](https://polyticktape.com/docs/): the same API with examples in curl, Python, pandas and polars. Chinese: https://polyticktape.com/zh/docs/. ## Optional - [Terms](https://polyticktape.com/terms/) - [Privacy](https://polyticktape.com/privacy/) - [Refund policy](https://polyticktape.com/refund/) - Contact: support@polyticktape.com --- # Poly Tick Tape API: guide for AI agents Base URL: `https://polyticktape.com`. Plain HTTPS and JSON. All dates are UTC days written `YYYY-MM-DD`; data starts on 2026-08-14. Timestamps in responses are Unix seconds. The files are paid. If the user has no key or the dates they need are not covered, tell them before writing download code: see "No key or no access" below, and the instructions in https://polyticktape.com/llms.txt. ## Authentication Every `/v1` request needs the user's API key: ``` Authorization: Bearer ptt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` - Keys match `^ptt_[a-z2-7]{32}$`. Read the key from the environment variable `POLYTICKTAPE_KEY`; never hard-code or print it. - One key per account. Purchases and renewals keep it; only a rotation on the account page replaces it (the old one then gets `401 key_revoked`). - `/v1/days/{date}/{stream}` also accepts `?key=` for tools that can only open a URL; no other route does. - The first call after a purchase is paid makes that purchase non-refundable. ## Recommended workflow 1. Get the key from `POLYTICKTAPE_KEY`. None: the user needs to buy first (see below). 2. `GET /v1/access`: read `ranges`, the date ranges this key can download now. If the needed dates are not inside them, stop and tell the user which periods to buy, or to subscribe. 3. `GET /v1/days?from=…&to=…` (at most 92 days per call; split longer ranges): gives `days[]`, where each day has one object per stream with `name`, `bytes`, `records`, `coverage`, `sha256` and download `url`, and `missing[]` for streams and days the key can't get. 4. For each file: if a local copy with the same SHA-256 exists, skip it (published files never change, and each download counts toward the daily limit). Otherwise GET its `url` following redirects, check the SHA-256, save. 5. Parse each file as gzip-compressed JSON Lines. Keep `p` as an exact decimal (string or Decimal); `v` is the same price as a float. ## No key or no access - Prices: one 30-day period $5.00, downloadable for 7 days after payment (for a period bought while still in progress, 7 days after its last day is published). Subscription $9.99/month or $99.99/year for the newest 60 published days. - Period of a date: `n = floor(days_between(2026-08-14, date) / 30) + 1`. Period n runs from 2026-08-14 + 30·(n−1) days for 30 days. - Buy at https://polyticktape.com/pricing/ after logging in with an email code at https://polyticktape.com/login/. The key is then on https://polyticktape.com/account/ (and emailed). - `GET /api/public/catalog` → `paymentsEnabled: false` means purchases are not open yet: tell the user to check back or write to support@polyticktape.com. ## Endpoints ### GET /api/public/catalog No key. What is published and what it costs: ```json { "brand": "Poly Tick Tape", "saleStart": "2026-08-14", "today": "2026-10-13", "firstDay": "2026-08-14", "lastDay": "2026-10-12", "updateDelayed": false, "totals": { "days": 60, "files": 120, "records": 9890000, "bytes": 310000000 }, "days": [{ "date": "2026-08-14", "price": { "records": 75392, "bytes": 2390000, "maxGap": 120 }, "twap60": { "records": 75392, "bytes": 2411945, "maxGap": 120 } }], "periods": [{ "n": 1, "start": "2026-08-14", "end": "2026-09-12", "status": "complete", "purchasable": true }], "prices": { "currency": "usd", "periodCents": 500, "monthlyCents": 999, "yearlyCents": 9999 }, "subWindowDays": 60, "paymentsEnabled": true } ``` ### GET /v1/access What this key can download right now. `streams` lists the stream names. `ranges` merges bought periods that are still within their download window with the subscription window. In `periods`, `expiresAt` is the end of that period's download window (`null` while a period bought in progress is not fully published) and `active` turns false after it. `subscription` is `null` without one; its `window` is `null` when not active. `quota` is today's download count. ```http GET /v1/access { "streams": ["price", "twap60"], "saleStart": "2026-08-14", "lastPublished": "2026-12-20", "firstDay": "2026-08-14", "lastDay": "2026-12-20", "ranges": [ { "from": "2026-08-14", "to": "2026-09-12" }, { "from": "2026-10-22", "to": "2026-12-20" } ], "periods": [ { "period": 1, "expiresAt": 1798099200, "active": true } ], "subscription": { "plan": "monthly", "status": "active", "active": true, "currentPeriodEnd": 1798761600, "cancelAtPeriodEnd": false, "graceUntil": null, "window": { "from": "2026-10-22", "to": "2026-12-20" } }, "quota": { "dailyDownloads": 300, "usedToday": 12 } } ``` ### GET /v1/days | Parameter | Meaning | |---|---| | `from` | Required. First day. | | `to` | Optional, inclusive; defaults to `from`. At most 92 days per call. | | `streams` | `price`, `twap60` or `price,twap60` (default both). | `from` is required; the latest published day is `lastPublished` in `/v1/access`. Each entry in `days` has `date` plus one object per requested stream it can serve. Streams the key can't get are listed in `missing` with `reason` `not_entitled` (outside access: the user must buy) or `not_published` (not out yet, or no data that day) instead of failing; a day with no stream available appears only there. `coverage` is the share of the day's 86,400 seconds that have a row. ```http GET /v1/days?from=2026-08-14&streams=twap60 { "from": "2026-08-14", "to": "2026-08-14", "streams": ["twap60"], "days": [ { "date": "2026-08-14", "twap60": { "name": "btc-twap60-2026-08-14.jsonl.gz", "bytes": 2411945, "records": 75392, "coverage": 0.8726, "sha256": "bef953289d716b3360d5afdd30db4902edf8849097f3291add330abb81605415", "url": "https://polyticktape.com/v1/days/2026-08-14/twap60" } } ], "missing": [] } ``` ```json "missing": [ { "date": "2026-09-13", "stream": "price", "reason": "not_entitled" }, { "date": "2026-10-12", "stream": "price", "reason": "not_published" } ] ``` ### GET /v1/days/{date}/{stream} `stream` is `price` or `twap60`. Checks access and the daily limit, counts one download, and answers `302` to a link valid for 5 minutes (header `X-Ptt-Sha256`). Follow redirects. The link needs no key, supports `Range` for resuming, and saves as `btc-{stream}-{date}.jsonl.gz`. Add `?as=json` to get JSON `{url, expiresAt, name, sha256, bytes, records}` instead. ``` GET /v1/days/2026-08-14/twap60 HTTP/1.1 302 Found Location: https://polyticktape.com/blob/eyJrIjoi… X-Ptt-Sha256: bef953289d716b3360d5afdd30db4902edf8849097f3291add330abb81605415 ``` ## File format - Names: `btc-price-YYYY-MM-DD.jsonl.gz`, `btc-twap60-YYYY-MM-DD.jsonl.gz`. About 2.3–2.7 MB and 75–86 thousand rows each. - gzip-compressed JSON Lines, UTF-8, one object per line, every line ends with `\n`. - One UTC day per file (`ts` from 00:00:00 to 23:59:59), sorted by `ts`, at most one row per second. Seconds without a quote have no row; `coverage` is the share of the day's 86,400 seconds that have one. - Published by 05:00 UTC the next day and never changed. Verify each file against `sha256`. | Field | Type | Meaning | |---|---|---| | `ts` | int | Second the quote belongs to (Unix seconds) = `feed_ts_ms // 1000` | | `p` | string | Price at full precision; compare and settle with this | | `v` | float | The same price as a float, for charts and statistics | | `lag` | int | `recv_ms - feed_ts_ms` | | `feed_ts_ms` | int | Quote time (ms) | | `server_ts_ms` | int or null | When the upstream server sent it (ms) | | `recv_ms` | int | When the collector received it (ms) | ```json {"ts":1786665600,"p":"63416.048159164624535552","v":63416.048159164624,"lag":2636,"feed_ts_ms":1786665600000,"server_ts_ms":1786665602338,"recv_ms":1786665602636} {"ts":1786665602,"p":"63416.671282124873007104","v":63416.67128212487,"lag":1945,"feed_ts_ms":1786665602000,"server_ts_ms":1786665603584,"recv_ms":1786665603945} ``` ## Errors Errors are JSON `{"error": {"code", "message", "hint"?}}`. React to `code`, not the message. | HTTP | code | What to do | |---|---|---| | 400 | `bad_request` | Fix the request (missing `from`, bad date, unknown stream, range over 92 days). | | 401 | `unauthorized` | No key or unknown key. Ask the user for their key; if they have none, they need to buy (https://polyticktape.com/pricing/). | | 401 | `key_revoked` | A newer key replaced this one. The user copies the current key from https://polyticktape.com/account/. | | 403 | `not_entitled` | Date outside the key's access. Show the user the `hint` and remind them to buy the period or subscribe at https://polyticktape.com/pricing/ (or fix a failed payment at https://polyticktape.com/account/). Don't retry. | | 404 | `not_found` | Unknown stream or path. | | 404 | `not_published` | Not published yet (by 05:00 UTC the next day). Retry later. | | 410 | `link_expired` | Signed link older than 5 minutes. Call `/v1/days/{date}/{stream}` again. | | 429 | `quota_exceeded` | 300 downloads per UTC day reached. `Retry-After` = seconds to 00:00 UTC. | | 503 | `payment_unavailable` | Purchases are not open (website checkout only). | | 500 | `internal` | Retry later; if it persists, the user can write to support@polyticktape.com. | ```json HTTP/1.1 403 Forbidden { "error": { "code": "not_entitled", "message": "Your key does not cover 2026-09-20.", "hint": "Buy period 2 or subscribe at https://polyticktape.com/pricing/." } } ``` ## Limits - 300 downloads per account per UTC day (each day has two files). `/v1/access` and the `/v1/days` listing do not count. - Signed links expire after 5 minutes. - `/v1/days` covers at most 92 days per call. - Download each file once and keep it. ## Python: download with checks Needs `pip install requests` and `POLYTICKTAPE_KEY` in the environment. Skips files already on disk and verifies every SHA-256. ```python import hashlib import os import pathlib import requests BASE = "https://polyticktape.com" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['POLYTICKTAPE_KEY']}" def download(date_from, date_to, streams="price,twap60", out="data"): r = session.get(f"{BASE}/v1/days", params={"from": date_from, "to": date_to, "streams": streams}) r.raise_for_status() listing = r.json() files = [day[s] for day in listing["days"] for s in listing["streams"] if s in day] for f in files: path = pathlib.Path(out) / f["name"] if path.exists() and hashlib.sha256(path.read_bytes()).hexdigest() == f["sha256"]: continue # already downloaded; don't spend quota on it again body = session.get(f["url"]).content # follows the redirect to a 5-minute link if hashlib.sha256(body).hexdigest() != f["sha256"]: raise ValueError(f"checksum mismatch: {f['name']}") path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(body) print("saved", path) for m in listing["missing"]: print("missing", m["date"], m["stream"], m["reason"]) download("2026-08-14", "2026-08-20") ``` Load with pandas (keep `dtype=False` so `p` stays exact) or polars: ```python import pandas as pd df = pd.read_json("data/btc-twap60-2026-08-14.jsonl.gz", lines=True, dtype=False, convert_dates=False) df["time"] = pd.to_datetime(df["ts"], unit="s", utc=True) # ts, lag and the *_ms columns are integers, p is a string, v is a float ``` ```python import gzip import polars as pl with gzip.open("data/btc-twap60-2026-08-14.jsonl.gz") as f: df = pl.read_ndjson(f.read()) df = df.with_columns(pl.from_epoch("ts", time_unit="s").dt.replace_time_zone("UTC").alias("time")) # p stays a String column: exact decimal digits ``` ## Check 5-minute rounds A round starts at a second divisible by 300 and ends at the next round's start. Up when TWAP60 at the end second >= TWAP60 at the start second (compare `p` as Decimal), otherwise Down. The day's last round ends at 00:00:00 of the next day, so load the next day's file too. If a boundary second has no row, report the round rather than guessing. ```python import gzip import json from datetime import datetime, timezone from decimal import Decimal def twap60(path): with gzip.open(path, "rt") as f: return {row["ts"]: Decimal(row["p"]) for row in map(json.loads, f)} # The last round of a day ends at 00:00 the next day, so load both files. twap = {**twap60("data/btc-twap60-2026-08-14.jsonl.gz"), **twap60("data/btc-twap60-2026-08-15.jsonl.gz")} day = int(datetime(2026, 8, 14, tzinfo=timezone.utc).timestamp()) for start in range(day, day + 86_400, 300): open_, close = twap.get(start), twap.get(start + 300) if open_ is None or close is None: print(start, "no TWAP60 row at a boundary second") continue print(start, "Up" if close >= open_ else "Down", open_, close) ```