pdata-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdata/__init__.py +29 -0
- pdata/_version.py +3 -0
- pdata/client.py +287 -0
- pdata/py.typed +0 -0
- pdata_client-0.1.0.dist-info/METADATA +220 -0
- pdata_client-0.1.0.dist-info/RECORD +9 -0
- pdata_client-0.1.0.dist-info/WHEEL +5 -0
- pdata_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- pdata_client-0.1.0.dist-info/top_level.txt +1 -0
pdata/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""A small Python client for pdata.world, the prediction-market data API.
|
|
2
|
+
|
|
3
|
+
pdata.world collects prices, volumes and results from eight venues —
|
|
4
|
+
Polymarket, Kalshi, Manifold, Myriad, Limitless, Predict, Opinion and
|
|
5
|
+
Gemini — and serves them through one read-only API. No key, no signup.
|
|
6
|
+
The data is published under CC BY 4.0: use it, and credit pdata.world.
|
|
7
|
+
|
|
8
|
+
>>> from pdata import Client
|
|
9
|
+
>>> pd = Client()
|
|
10
|
+
>>> page = pd.markets(source="kalshi", sort="-volume_24hr", page_size=5)
|
|
11
|
+
>>> [m["question"] for m in page["items"]]
|
|
12
|
+
|
|
13
|
+
Standard library only (urllib + json + gzip), Python 3.9+.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
from .client import API_BASE, BULK_BASE, USER_AGENT, VENUES, Client, PdataError
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"API_BASE",
|
|
23
|
+
"BULK_BASE",
|
|
24
|
+
"USER_AGENT",
|
|
25
|
+
"VENUES",
|
|
26
|
+
"Client",
|
|
27
|
+
"PdataError",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
pdata/_version.py
ADDED
pdata/client.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""The client: one method per pdata.world API operation, plus the bulk files.
|
|
2
|
+
|
|
3
|
+
Every method returns the API's JSON as plain dicts and lists, unchanged, so
|
|
4
|
+
the API reference at https://api.pdata.world/docs describes the fields.
|
|
5
|
+
List endpoints return a page: ``{"items": [...], "meta": {"page", "page_size",
|
|
6
|
+
"total", "total_pages"}, "_meta": {...}}``; the ``iter_*`` methods walk the
|
|
7
|
+
pages for you.
|
|
8
|
+
|
|
9
|
+
Units: ``probability`` and prices are 0-1. ``volume`` and ``volume_24hr``
|
|
10
|
+
are in each venue's own unit and are not converted across venues — shares
|
|
11
|
+
for Polymarket, contracts for Kalshi (each pays $1 at resolution), play-money
|
|
12
|
+
mana for Manifold.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import gzip
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Iterator, Mapping
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
from urllib.error import HTTPError, URLError
|
|
23
|
+
from urllib.parse import quote, urlencode
|
|
24
|
+
from urllib.request import Request, urlopen
|
|
25
|
+
|
|
26
|
+
from ._version import __version__
|
|
27
|
+
|
|
28
|
+
API_BASE = "https://api.pdata.world/api/v1"
|
|
29
|
+
BULK_BASE = "https://api.pdata.world/bulk"
|
|
30
|
+
# Sent unless the caller passes its own; names the client and where to read
|
|
31
|
+
# about pdata.world's agent and API access.
|
|
32
|
+
USER_AGENT = f"pdata-client/{__version__} (+https://pdata.world/agents)"
|
|
33
|
+
VENUES = (
|
|
34
|
+
"polymarket",
|
|
35
|
+
"kalshi",
|
|
36
|
+
"manifold",
|
|
37
|
+
"myriad",
|
|
38
|
+
"limitless",
|
|
39
|
+
"predict",
|
|
40
|
+
"opinion",
|
|
41
|
+
"gemini",
|
|
42
|
+
)
|
|
43
|
+
BULK_KINDS = ("markets-open", "events-open", "resolved")
|
|
44
|
+
|
|
45
|
+
# The API caps page_size at 200.
|
|
46
|
+
MAX_PAGE_SIZE = 200
|
|
47
|
+
_RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
|
|
48
|
+
|
|
49
|
+
JSON = dict[str, Any]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PdataError(Exception):
|
|
53
|
+
"""An error answer from the API.
|
|
54
|
+
|
|
55
|
+
``code`` is the API's machine-readable code (``not_found``,
|
|
56
|
+
``unknown_param``, ``invalid_source``, ...) and ``message`` its
|
|
57
|
+
explanation. An ``unknown_param`` message lists every allowed parameter.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, status: int, code: str, message: str) -> None:
|
|
61
|
+
super().__init__(f"{status} {code}: {message}")
|
|
62
|
+
self.status = status
|
|
63
|
+
self.code = code
|
|
64
|
+
self.message = message
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _encode(params: Mapping[str, Any]) -> str:
|
|
68
|
+
"""Query string: None dropped, booleans as true/false, sequences comma-joined."""
|
|
69
|
+
out: list[tuple[str, str]] = []
|
|
70
|
+
for key, value in params.items():
|
|
71
|
+
if value is None:
|
|
72
|
+
continue
|
|
73
|
+
if isinstance(value, bool):
|
|
74
|
+
out.append((key, "true" if value else "false"))
|
|
75
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
76
|
+
out.append((key, ",".join(str(v) for v in value)))
|
|
77
|
+
else:
|
|
78
|
+
out.append((key, str(value)))
|
|
79
|
+
return urlencode(out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _seg(value: str) -> str:
|
|
83
|
+
"""One URL path segment (ids can hold characters that need escaping)."""
|
|
84
|
+
return quote(str(value), safe="")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Client:
|
|
88
|
+
"""pdata.world API client.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
base_url: API root, default ``https://api.pdata.world/api/v1``.
|
|
92
|
+
bulk_url: bulk-file root, default ``https://api.pdata.world/bulk``.
|
|
93
|
+
timeout: seconds per request.
|
|
94
|
+
retries: extra attempts on 429/5xx and network errors, with backoff.
|
|
95
|
+
user_agent: sent on every request instead of ``USER_AGENT``; if you
|
|
96
|
+
set one, name your project in it.
|
|
97
|
+
opener: a ``urlopen``-compatible callable, for tests or a custom
|
|
98
|
+
transport.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
base_url: str = API_BASE,
|
|
104
|
+
*,
|
|
105
|
+
bulk_url: str = BULK_BASE,
|
|
106
|
+
timeout: float = 30.0,
|
|
107
|
+
retries: int = 2,
|
|
108
|
+
user_agent: str | None = None,
|
|
109
|
+
opener: Callable[..., Any] | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
self.base_url = base_url.rstrip("/")
|
|
112
|
+
self.bulk_url = bulk_url.rstrip("/")
|
|
113
|
+
self.timeout = timeout
|
|
114
|
+
self.retries = retries
|
|
115
|
+
self.user_agent = user_agent or USER_AGENT
|
|
116
|
+
self._opener = opener or urlopen
|
|
117
|
+
|
|
118
|
+
# ── transport ────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def _open(self, url: str) -> Any:
|
|
121
|
+
request = Request(url, headers={"User-Agent": self.user_agent, "Accept": "*/*"})
|
|
122
|
+
attempt = 0
|
|
123
|
+
while True:
|
|
124
|
+
try:
|
|
125
|
+
return self._opener(request, timeout=self.timeout)
|
|
126
|
+
except HTTPError as err:
|
|
127
|
+
if err.code in _RETRY_STATUSES and attempt < self.retries:
|
|
128
|
+
retry_after = err.headers.get("Retry-After") if err.headers else None
|
|
129
|
+
time.sleep(float(retry_after) if retry_after else 1.5 * 2**attempt)
|
|
130
|
+
attempt += 1
|
|
131
|
+
continue
|
|
132
|
+
raise self._error(err) from None
|
|
133
|
+
except URLError:
|
|
134
|
+
if attempt < self.retries:
|
|
135
|
+
time.sleep(1.5 * 2**attempt)
|
|
136
|
+
attempt += 1
|
|
137
|
+
continue
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _error(err: HTTPError) -> PdataError:
|
|
142
|
+
try:
|
|
143
|
+
body = json.loads(err.read().decode("utf-8"))
|
|
144
|
+
except (ValueError, OSError):
|
|
145
|
+
body = {}
|
|
146
|
+
code = body.get("code") or "http_error"
|
|
147
|
+
message = body.get("message")
|
|
148
|
+
if message is None and "detail" in body: # request validation errors
|
|
149
|
+
message = json.dumps(body["detail"])
|
|
150
|
+
return PdataError(err.code, code, message or str(err.reason))
|
|
151
|
+
|
|
152
|
+
def get(self, path: str, **params: Any) -> Any:
|
|
153
|
+
"""GET ``base_url + path`` with ``params``; returns the decoded JSON."""
|
|
154
|
+
query = _encode(params)
|
|
155
|
+
url = f"{self.base_url}{path}" + (f"?{query}" if query else "")
|
|
156
|
+
with self._open(url) as resp:
|
|
157
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
158
|
+
|
|
159
|
+
def _iterate(self, path: str, max_items: int | None, params: dict[str, Any]) -> Iterator[JSON]:
|
|
160
|
+
params.setdefault("page_size", MAX_PAGE_SIZE)
|
|
161
|
+
page = int(params.pop("page", 1))
|
|
162
|
+
seen = 0
|
|
163
|
+
while True:
|
|
164
|
+
body = self.get(path, page=page, **params)
|
|
165
|
+
for item in body.get("items", []):
|
|
166
|
+
yield item
|
|
167
|
+
seen += 1
|
|
168
|
+
if max_items is not None and seen >= max_items:
|
|
169
|
+
return
|
|
170
|
+
if page >= int((body.get("meta") or {}).get("total_pages") or page):
|
|
171
|
+
return
|
|
172
|
+
page += 1
|
|
173
|
+
|
|
174
|
+
# ── markets ──────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def markets(self, **filters: Any) -> JSON:
|
|
177
|
+
"""One page of markets (``GET /markets``).
|
|
178
|
+
|
|
179
|
+
Common filters: ``source`` (a venue or a list), ``search``,
|
|
180
|
+
``categories``, ``closed``, ``probability_min``/``_max``,
|
|
181
|
+
``volume_24hr_min``, ``ends_before``/``ends_after`` (ISO 8601),
|
|
182
|
+
``sort`` (e.g. ``"-volume_24hr"``, ``"-prob_24h_change"``), ``page``,
|
|
183
|
+
``page_size`` (max 200). Unknown parameters are rejected with the
|
|
184
|
+
full list of allowed ones.
|
|
185
|
+
"""
|
|
186
|
+
return self.get("/markets", **filters)
|
|
187
|
+
|
|
188
|
+
def iter_markets(self, max_items: int | None = None, **filters: Any) -> Iterator[JSON]:
|
|
189
|
+
"""Every market matching ``filters``, page by page."""
|
|
190
|
+
return self._iterate("/markets", max_items, dict(filters))
|
|
191
|
+
|
|
192
|
+
def market(self, source: str, market_id: str) -> JSON:
|
|
193
|
+
"""One market by venue and id (``GET /markets/{source}/{market_id}``)."""
|
|
194
|
+
return self.get(f"/markets/{_seg(source)}/{_seg(market_id)}")
|
|
195
|
+
|
|
196
|
+
def top_movers(self, limit: int | None = None, **filters: Any) -> JSON:
|
|
197
|
+
"""Markets with the largest probability change over 24 hours."""
|
|
198
|
+
return self.get("/markets/top-movers", limit=limit, **filters)
|
|
199
|
+
|
|
200
|
+
def closing_soon(self, limit: int | None = None, **filters: Any) -> JSON:
|
|
201
|
+
"""Open markets closing soonest."""
|
|
202
|
+
return self.get("/markets/closing-soon", limit=limit, **filters)
|
|
203
|
+
|
|
204
|
+
# ── events ───────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def events(self, **filters: Any) -> JSON:
|
|
207
|
+
"""One page of events (``GET /events``); an event groups the markets of one question."""
|
|
208
|
+
return self.get("/events", **filters)
|
|
209
|
+
|
|
210
|
+
def iter_events(self, max_items: int | None = None, **filters: Any) -> Iterator[JSON]:
|
|
211
|
+
"""Every event matching ``filters``, page by page."""
|
|
212
|
+
return self._iterate("/events", max_items, dict(filters))
|
|
213
|
+
|
|
214
|
+
def event(self, source: str, event_id: str, markets_limit: int | None = None) -> JSON:
|
|
215
|
+
"""One event with its markets.
|
|
216
|
+
|
|
217
|
+
Events closed more than 10 days ago come back as
|
|
218
|
+
``{"kind": "tombstone", ...}`` with each top market's final result;
|
|
219
|
+
live ones as ``{"kind": "live", ...}``. Check ``kind`` first.
|
|
220
|
+
``_meta.cite_as`` holds a ready-made citation sentence.
|
|
221
|
+
"""
|
|
222
|
+
return self.get(
|
|
223
|
+
f"/events/{_seg(source)}/{_seg(event_id)}", markets_limit=markets_limit
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def event_history(
|
|
227
|
+
self,
|
|
228
|
+
source: str,
|
|
229
|
+
event_id: str,
|
|
230
|
+
range: str = "24h",
|
|
231
|
+
market_id: str | None = None,
|
|
232
|
+
limit: int | None = None,
|
|
233
|
+
) -> JSON:
|
|
234
|
+
"""Price and 24h-volume series for an event's top markets.
|
|
235
|
+
|
|
236
|
+
``range`` is ``"24h"``, ``"7d"``, ``"30d"`` or ``"all"`` (history
|
|
237
|
+
starts May 2026; points older than 7 days are 12-hour buckets).
|
|
238
|
+
"""
|
|
239
|
+
return self.get(
|
|
240
|
+
f"/events/{_seg(source)}/{_seg(event_id)}/history",
|
|
241
|
+
range=range,
|
|
242
|
+
market_id=market_id,
|
|
243
|
+
limit=limit,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def similar_events(self, source: str, event_id: str, limit: int | None = None) -> JSON:
|
|
247
|
+
"""Events on other venues (and the same one) asking a similar question."""
|
|
248
|
+
return self.get(f"/events/{_seg(source)}/{_seg(event_id)}/similar", limit=limit)
|
|
249
|
+
|
|
250
|
+
# ── search, summary, news ───────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
def search(self, q: str, limit: int | None = None) -> JSON:
|
|
253
|
+
"""Events and markets whose titles match ``q``."""
|
|
254
|
+
return self.get("/search/suggest", q=q, limit=limit)
|
|
255
|
+
|
|
256
|
+
def summary(self) -> JSON:
|
|
257
|
+
"""Per-venue totals: open markets, events and 24h volume."""
|
|
258
|
+
return self.get("/summary")
|
|
259
|
+
|
|
260
|
+
def news_movers(self, **filters: Any) -> JSON:
|
|
261
|
+
"""The /news feed: markets whose price moved most, scored by move and volume."""
|
|
262
|
+
return self.get("/news/movers", **filters)
|
|
263
|
+
|
|
264
|
+
# ── bulk files ───────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
def bulk_index(self) -> JSON:
|
|
267
|
+
"""The bulk-file index: each file's URL, row count, size, sha256 and time."""
|
|
268
|
+
with self._open(f"{self.bulk_url}/index.json") as resp:
|
|
269
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
270
|
+
|
|
271
|
+
def iter_bulk(self, kind: str, *, include_manifest: bool = False) -> Iterator[JSON]:
|
|
272
|
+
"""Stream a nightly bulk file, one record at a time.
|
|
273
|
+
|
|
274
|
+
``kind`` is ``"markets-open"``, ``"events-open"`` or ``"resolved"``.
|
|
275
|
+
Line 1 of each file is a manifest (``{"_manifest": {...}}``) giving
|
|
276
|
+
the licence and every field's meaning and unit; it is skipped unless
|
|
277
|
+
``include_manifest`` is true. Nothing is written to disk.
|
|
278
|
+
"""
|
|
279
|
+
if kind not in BULK_KINDS:
|
|
280
|
+
raise ValueError(f"kind must be one of {BULK_KINDS}, not {kind!r}")
|
|
281
|
+
with self._open(f"{self.bulk_url}/{kind}-latest.jsonl.gz") as resp:
|
|
282
|
+
with gzip.GzipFile(fileobj=resp) as lines:
|
|
283
|
+
for i, line in enumerate(lines):
|
|
284
|
+
row = json.loads(line)
|
|
285
|
+
if i == 0 and "_manifest" in row and not include_manifest:
|
|
286
|
+
continue
|
|
287
|
+
yield row
|
pdata/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdata-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for pdata.world, a free read-only API for prediction-market data from Polymarket, Kalshi, Manifold and five more venues.
|
|
5
|
+
Author-email: "pdata.world" <support@pdata.world>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://pdata.world
|
|
8
|
+
Project-URL: Documentation, https://api.pdata.world/docs
|
|
9
|
+
Project-URL: OpenAPI, https://api.pdata.world/openapi.json
|
|
10
|
+
Project-URL: Repository, https://github.com/noskillcoding/pdata-client
|
|
11
|
+
Project-URL: Issues, https://github.com/noskillcoding/pdata-client/issues
|
|
12
|
+
Project-URL: Data, https://pdata.world/data
|
|
13
|
+
Project-URL: Research, https://pdata.world/research
|
|
14
|
+
Keywords: prediction markets,polymarket,kalshi,manifold,api client,open data
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.9
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# pdata-client
|
|
36
|
+
|
|
37
|
+
Python client, AI-assistant plugin files and sample data for **[pdata.world](https://pdata.world)**,
|
|
38
|
+
a free, read-only API for prediction-market data from eight venues: Polymarket, Kalshi,
|
|
39
|
+
Manifold, Myriad, Limitless, Predict, Opinion and Gemini.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install pdata-client
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pdata import Client
|
|
47
|
+
|
|
48
|
+
pd = Client()
|
|
49
|
+
page = pd.markets(source=["kalshi", "polymarket"], closed=False, sort="-volume_24hr", page_size=5)
|
|
50
|
+
for m in page["items"]:
|
|
51
|
+
print(m["source"], f"{m['probability']:.0%}", m["event_title"], "—", m["question"])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Cite as:** Data aggregated by pdata.world (https://pdata.world) from 8 prediction-market
|
|
55
|
+
venues; CC BY 4.0; source data © each venue. Dataset DOI
|
|
56
|
+
[10.5281/zenodo.23211610](https://doi.org/10.5281/zenodo.23211610).
|
|
57
|
+
|
|
58
|
+
pdata.world polls each venue every 10 to 60 minutes and keeps prices, 24-hour volume and
|
|
59
|
+
results in one schema, so one request can compare venues that otherwise each need their own
|
|
60
|
+
integration. There is no API key and no signup. The data is published under
|
|
61
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): use it, and credit pdata.world.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install pdata-client
|
|
67
|
+
# or the latest commit:
|
|
68
|
+
pip install git+https://github.com/noskillcoding/pdata-client
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The client uses only the standard library (`urllib`, `json`, `gzip`), has no dependencies
|
|
72
|
+
and runs on Python 3.9 or later. You can also copy
|
|
73
|
+
[`src/pdata/client.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/client.py) and
|
|
74
|
+
[`src/pdata/_version.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/_version.py) into a project. It identifies itself as
|
|
75
|
+
`pdata-client/<version> (+https://pdata.world/agents)`; pass `user_agent=` to name your own
|
|
76
|
+
project instead.
|
|
77
|
+
|
|
78
|
+
## The MCP server and the assistant plugins
|
|
79
|
+
|
|
80
|
+
pdata.world hosts an MCP server at `https://api.pdata.world/mcp` (Streamable HTTP, no key,
|
|
81
|
+
no account). It is **read-only**: its 14 tools read the same public data as the API. The
|
|
82
|
+
server is run by pdata.world and its code is **closed-source**; it is not in this
|
|
83
|
+
repository. This repository holds the Python client, the plugin files that connect coding
|
|
84
|
+
assistants to the hosted server, and notes on the datasets. The server is listed in the
|
|
85
|
+
official MCP registry as `world.pdata/mcp`.
|
|
86
|
+
|
|
87
|
+
- **Claude Code:** `/plugin install pdata --marketplace noskillcoding/pdata-client` inside a
|
|
88
|
+
session (Claude Code 2.1.275 or later), or from the shell
|
|
89
|
+
`claude plugin marketplace add noskillcoding/pdata-client` followed by
|
|
90
|
+
`claude plugin install pdata@pdata-world`. The plugin adds the MCP server and a skill,
|
|
91
|
+
[`skills/pdata-prediction-markets/SKILL.md`](https://github.com/noskillcoding/pdata-client/blob/main/skills/pdata-prediction-markets/SKILL.md),
|
|
92
|
+
that tells the assistant when to use pdata and how to cite it.
|
|
93
|
+
- **Gemini CLI:** `gemini extensions install https://github.com/noskillcoding/pdata-client`
|
|
94
|
+
adds the same server and skill.
|
|
95
|
+
- **Any other MCP client:** add the server by URL, for example
|
|
96
|
+
`claude mcp add --transport http pdata https://api.pdata.world/mcp`, or the config in
|
|
97
|
+
[`examples/mcp.json`](https://github.com/noskillcoding/pdata-client/blob/main/examples/mcp.json).
|
|
98
|
+
|
|
99
|
+
## What is in this repository
|
|
100
|
+
|
|
101
|
+
| Path | What it is |
|
|
102
|
+
|---|---|
|
|
103
|
+
| [`src/pdata/`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/) | The Python client, published on PyPI as `pdata-client` |
|
|
104
|
+
| [`.claude-plugin/`](https://github.com/noskillcoding/pdata-client/blob/main/.claude-plugin/), [`.mcp.json`](https://github.com/noskillcoding/pdata-client/blob/main/.mcp.json), [`skills/`](https://github.com/noskillcoding/pdata-client/blob/main/skills/) | Claude Code plugin and its one-plugin marketplace |
|
|
105
|
+
| [`gemini-extension.json`](https://github.com/noskillcoding/pdata-client/blob/main/gemini-extension.json) | Gemini CLI extension (uses the same `skills/`) |
|
|
106
|
+
| [`apify/`](https://github.com/noskillcoding/pdata-client/blob/main/apify/) | Source of an Apify actor that returns events with current prices |
|
|
107
|
+
| [`data/`](https://github.com/noskillcoding/pdata-client/blob/main/data/) | Samples of the nightly bulk files, CC BY 4.0 |
|
|
108
|
+
| [`examples/`](https://github.com/noskillcoding/pdata-client/blob/main/examples/) | Runnable scripts and an MCP client config |
|
|
109
|
+
|
|
110
|
+
## What the client covers
|
|
111
|
+
|
|
112
|
+
| Method | API operation | What it returns |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `markets(**filters)`, `iter_markets(...)` | `GET /markets` | Markets, filtered and sorted, one page or all pages |
|
|
115
|
+
| `market(source, id)` | `GET /markets/{source}/{id}` | One market |
|
|
116
|
+
| `top_movers(...)`, `closing_soon(...)` | `GET /markets/top-movers`, `/closing-soon` | Largest 24h price changes; markets closing next |
|
|
117
|
+
| `events(**filters)`, `iter_events(...)` | `GET /events` | Events (an event groups the markets of one question) |
|
|
118
|
+
| `event(source, id)` | `GET /events/{source}/{id}` | One event with its markets, or its final results once archived |
|
|
119
|
+
| `event_history(source, id, range=...)` | `GET /events/{source}/{id}/history` | Price and 24h-volume series, `24h` / `7d` / `30d` / `all` |
|
|
120
|
+
| `similar_events(source, id)` | `GET /events/{source}/{id}/similar` | The same or similar question on other venues |
|
|
121
|
+
| `search(q)` | `GET /search/suggest` | Events and markets by title |
|
|
122
|
+
| `summary()` | `GET /summary` | Per-venue counts and 24h volume |
|
|
123
|
+
| `news_movers(...)` | `GET /news/movers` | The price moves behind pdata.world/news |
|
|
124
|
+
| `bulk_index()`, `iter_bulk(kind)` | `/bulk/` | Nightly full files, streamed without saving |
|
|
125
|
+
|
|
126
|
+
Every method returns the API's JSON unchanged, so the
|
|
127
|
+
[API reference](https://api.pdata.world/docs) describes every field. Unknown filters are
|
|
128
|
+
rejected with a message listing every allowed one. Errors raise `PdataError` with the API's
|
|
129
|
+
`code` and `message`.
|
|
130
|
+
|
|
131
|
+
Runnable examples are in [`examples/`](https://github.com/noskillcoding/pdata-client/blob/main/examples/):
|
|
132
|
+
[`quickstart.py`](https://github.com/noskillcoding/pdata-client/blob/main/examples/quickstart.py) makes five common calls, and
|
|
133
|
+
[`resolved_archive.py`](https://github.com/noskillcoding/pdata-client/blob/main/examples/resolved_archive.py) streams the archive of settled events.
|
|
134
|
+
|
|
135
|
+
## Other ways to get the same data
|
|
136
|
+
|
|
137
|
+
- **MCP server for AI assistants:** `https://api.pdata.world/mcp`; see
|
|
138
|
+
[above](#the-mcp-server-and-the-assistant-plugins).
|
|
139
|
+
- **Nightly bulk files** at <https://api.pdata.world/bulk/>: every open market
|
|
140
|
+
(`markets-open`), every open event (`events-open`) and the archive of settled events with
|
|
141
|
+
their results (`resolved`), as gzipped JSON lines. Line 1 of each file is a manifest
|
|
142
|
+
giving the licence and every field's meaning and unit.
|
|
143
|
+
- **Monthly snapshot on Hugging Face:** the same three files as Parquet, each snapshot
|
|
144
|
+
tagged with its date:
|
|
145
|
+
[`lowskillcoding/pdata-prediction-markets`](https://huggingface.co/datasets/lowskillcoding/pdata-prediction-markets).
|
|
146
|
+
- **Read-only Postgres** with full SQL:
|
|
147
|
+
`postgresql://pdata_readonly:pdata_public_2026@pg.pdata.world:6433/pdata_new`. The
|
|
148
|
+
password is published on purpose and is never rotated. Queries are capped at 5 seconds.
|
|
149
|
+
See [`examples/postgres.sql`](https://github.com/noskillcoding/pdata-client/blob/main/examples/postgres.sql).
|
|
150
|
+
- **The website:** every event has a page at `https://pdata.world/events/{source}/{id}`,
|
|
151
|
+
and <https://pdata.world/data> describes all of the above.
|
|
152
|
+
|
|
153
|
+
## Sample data
|
|
154
|
+
|
|
155
|
+
[`data/`](https://github.com/noskillcoding/pdata-client/blob/main/data/) holds random samples of the three bulk files as of 7 October 2026: 1,000
|
|
156
|
+
open markets, 500 open events and 1,000 settled events, with an equal share per venue where
|
|
157
|
+
the venue had enough rows. Each file starts with the manifest line. They are for looking at
|
|
158
|
+
the shape of the data; the full files are rebuilt every night.
|
|
159
|
+
|
|
160
|
+
## Units and things to know
|
|
161
|
+
|
|
162
|
+
- `probability` and prices are on a 0–1 scale.
|
|
163
|
+
- `volume` and `volume_24hr` are in each venue's own unit and are **not** converted across
|
|
164
|
+
venues: shares for Polymarket and contracts for Kalshi (each pays $1 at resolution),
|
|
165
|
+
play-money mana for Manifold, and each other venue's figure as it reports it.
|
|
166
|
+
[How eight venues count volume](https://pdata.world/research/prediction-market-volume)
|
|
167
|
+
explains each one.
|
|
168
|
+
- Price history starts in May 2026. Points older than 7 days are kept as 12-hour buckets,
|
|
169
|
+
and a market's history is deleted 10 days after it closes. After that, the event's URL
|
|
170
|
+
returns `"kind": "tombstone"` with each top market's final result.
|
|
171
|
+
- In the `resolved` archive, each market's `probability` is the last price pdata saw, which
|
|
172
|
+
is usually after the market settled. It records the result; it is not a forecast.
|
|
173
|
+
- Events that closed between 2 June and 11 August 2026 are missing from the `resolved`
|
|
174
|
+
archive: they were pruned before a backup was taken.
|
|
175
|
+
- pdata groups Kalshi's sibling events for one game under a single group event
|
|
176
|
+
(`is_group`); its volume is the sum of its children.
|
|
177
|
+
|
|
178
|
+
## Research using this data
|
|
179
|
+
|
|
180
|
+
pdata.world publishes dated reports built from this data, each with its method and limits:
|
|
181
|
+
|
|
182
|
+
- [How accurate were prediction-market prices a day before the result?](https://pdata.world/research/prediction-market-accuracy)
|
|
183
|
+
For 39,681 Polymarket and Kalshi markets that settled between 28 September and
|
|
184
|
+
5 October 2026, grouped into 10-point bands by their price 24 hours before close, the
|
|
185
|
+
share that resolved YES was on average 0.96 percentage points from the band's average
|
|
186
|
+
price (Brier score 0.110, against 0.199 for always guessing the base rate). Also published
|
|
187
|
+
as a citable note: [doi.org/10.5281/zenodo.23212130](https://doi.org/10.5281/zenodo.23212130).
|
|
188
|
+
- [How eight prediction-market venues count volume](https://pdata.world/research/prediction-market-volume)
|
|
189
|
+
- [What each prediction-market API gives you](https://pdata.world/research/prediction-market-apis)
|
|
190
|
+
- [Same question, different price: prediction markets across venues](https://pdata.world/research/cross-venue-prices)
|
|
191
|
+
- [What eight prediction-market venues list, by category](https://pdata.world/research/prediction-market-coverage)
|
|
192
|
+
|
|
193
|
+
## Citing
|
|
194
|
+
|
|
195
|
+
The data is CC BY 4.0. A credit such as "Data: pdata.world" with a link is enough. Every
|
|
196
|
+
single-event API answer also carries `_meta.cite_as`, a ready-made citation sentence. For
|
|
197
|
+
papers, cite the dataset on Zenodo: DOI
|
|
198
|
+
[10.5281/zenodo.23211610](https://doi.org/10.5281/zenodo.23211610) always resolves to the
|
|
199
|
+
latest snapshot, and each snapshot has its own DOI, such as
|
|
200
|
+
[10.5281/zenodo.23211611](https://doi.org/10.5281/zenodo.23211611) for 7 October 2026. The
|
|
201
|
+
calibration note is [10.5281/zenodo.23212130](https://doi.org/10.5281/zenodo.23212130).
|
|
202
|
+
GitHub's "Cite this repository" button uses [`CITATION.cff`](https://github.com/noskillcoding/pdata-client/blob/main/CITATION.cff).
|
|
203
|
+
|
|
204
|
+
## Licence
|
|
205
|
+
|
|
206
|
+
The code in this repository, including the plugin files and the Apify actor, is under the
|
|
207
|
+
[MIT licence](https://github.com/noskillcoding/pdata-client/blob/main/LICENSE). The data, including the files in `data/`, is under
|
|
208
|
+
[CC BY 4.0](https://github.com/noskillcoding/pdata-client/blob/main/data/LICENSE.md); the underlying market data remains subject to each venue's
|
|
209
|
+
own terms.
|
|
210
|
+
|
|
211
|
+
pdata.world is independent and not affiliated with any of the venues it tracks.
|
|
212
|
+
|
|
213
|
+
## Releasing
|
|
214
|
+
|
|
215
|
+
One version covers the PyPI package, the Claude Code plugin and the Gemini extension. Bump
|
|
216
|
+
it in [`src/pdata/_version.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/_version.py),
|
|
217
|
+
[`.claude-plugin/plugin.json`](https://github.com/noskillcoding/pdata-client/blob/main/.claude-plugin/plugin.json) and
|
|
218
|
+
[`gemini-extension.json`](https://github.com/noskillcoding/pdata-client/blob/main/gemini-extension.json) together (a test fails otherwise), then
|
|
219
|
+
push a tag `v<version>`: [`publish.yml`](https://github.com/noskillcoding/pdata-client/blob/main/.github/workflows/publish.yml) builds, tests and
|
|
220
|
+
uploads to PyPI through trusted publishing. Contact: support@pdata.world.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pdata/__init__.py,sha256=1-FYMwC_sO0VWQCWi9wev8VCHVaWX7DhVaQ3P-CLogc,886
|
|
2
|
+
pdata/_version.py,sha256=VDUr8he49zIUw29_8mMlYhbA0Ro1VaF266tuUpzR458,100
|
|
3
|
+
pdata/client.py,sha256=v2QQiZA1fK2Qipe7hQnDlUx2yOy8284UXDtk3u_DoZI,11978
|
|
4
|
+
pdata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
pdata_client-0.1.0.dist-info/licenses/LICENSE,sha256=P4PusSm-X1RpcDMUM15WrnVb-6mQ9zmDnMA2Rr_RmVI,1068
|
|
6
|
+
pdata_client-0.1.0.dist-info/METADATA,sha256=2G9hEMISM3KRcCx_vlf6kfCc8oXSxjNkQAWl1BiNRLI,13487
|
|
7
|
+
pdata_client-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
pdata_client-0.1.0.dist-info/top_level.txt,sha256=f1qR9sFanBfpYX5lj2A6c3lvHfBA4tw-_lGKSrzbl14,6
|
|
9
|
+
pdata_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pdata.world
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pdata
|