pdata-client 0.1.0__tar.gz

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.
@@ -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,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,186 @@
1
+ # pdata-client
2
+
3
+ Python client, AI-assistant plugin files and sample data for **[pdata.world](https://pdata.world)**,
4
+ a free, read-only API for prediction-market data from eight venues: Polymarket, Kalshi,
5
+ Manifold, Myriad, Limitless, Predict, Opinion and Gemini.
6
+
7
+ ```bash
8
+ pip install pdata-client
9
+ ```
10
+
11
+ ```python
12
+ from pdata import Client
13
+
14
+ pd = Client()
15
+ page = pd.markets(source=["kalshi", "polymarket"], closed=False, sort="-volume_24hr", page_size=5)
16
+ for m in page["items"]:
17
+ print(m["source"], f"{m['probability']:.0%}", m["event_title"], "—", m["question"])
18
+ ```
19
+
20
+ **Cite as:** Data aggregated by pdata.world (https://pdata.world) from 8 prediction-market
21
+ venues; CC BY 4.0; source data © each venue. Dataset DOI
22
+ [10.5281/zenodo.23211610](https://doi.org/10.5281/zenodo.23211610).
23
+
24
+ pdata.world polls each venue every 10 to 60 minutes and keeps prices, 24-hour volume and
25
+ results in one schema, so one request can compare venues that otherwise each need their own
26
+ integration. There is no API key and no signup. The data is published under
27
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): use it, and credit pdata.world.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install pdata-client
33
+ # or the latest commit:
34
+ pip install git+https://github.com/noskillcoding/pdata-client
35
+ ```
36
+
37
+ The client uses only the standard library (`urllib`, `json`, `gzip`), has no dependencies
38
+ and runs on Python 3.9 or later. You can also copy
39
+ [`src/pdata/client.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/client.py) and
40
+ [`src/pdata/_version.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/_version.py) into a project. It identifies itself as
41
+ `pdata-client/<version> (+https://pdata.world/agents)`; pass `user_agent=` to name your own
42
+ project instead.
43
+
44
+ ## The MCP server and the assistant plugins
45
+
46
+ pdata.world hosts an MCP server at `https://api.pdata.world/mcp` (Streamable HTTP, no key,
47
+ no account). It is **read-only**: its 14 tools read the same public data as the API. The
48
+ server is run by pdata.world and its code is **closed-source**; it is not in this
49
+ repository. This repository holds the Python client, the plugin files that connect coding
50
+ assistants to the hosted server, and notes on the datasets. The server is listed in the
51
+ official MCP registry as `world.pdata/mcp`.
52
+
53
+ - **Claude Code:** `/plugin install pdata --marketplace noskillcoding/pdata-client` inside a
54
+ session (Claude Code 2.1.275 or later), or from the shell
55
+ `claude plugin marketplace add noskillcoding/pdata-client` followed by
56
+ `claude plugin install pdata@pdata-world`. The plugin adds the MCP server and a skill,
57
+ [`skills/pdata-prediction-markets/SKILL.md`](https://github.com/noskillcoding/pdata-client/blob/main/skills/pdata-prediction-markets/SKILL.md),
58
+ that tells the assistant when to use pdata and how to cite it.
59
+ - **Gemini CLI:** `gemini extensions install https://github.com/noskillcoding/pdata-client`
60
+ adds the same server and skill.
61
+ - **Any other MCP client:** add the server by URL, for example
62
+ `claude mcp add --transport http pdata https://api.pdata.world/mcp`, or the config in
63
+ [`examples/mcp.json`](https://github.com/noskillcoding/pdata-client/blob/main/examples/mcp.json).
64
+
65
+ ## What is in this repository
66
+
67
+ | Path | What it is |
68
+ |---|---|
69
+ | [`src/pdata/`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/) | The Python client, published on PyPI as `pdata-client` |
70
+ | [`.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 |
71
+ | [`gemini-extension.json`](https://github.com/noskillcoding/pdata-client/blob/main/gemini-extension.json) | Gemini CLI extension (uses the same `skills/`) |
72
+ | [`apify/`](https://github.com/noskillcoding/pdata-client/blob/main/apify/) | Source of an Apify actor that returns events with current prices |
73
+ | [`data/`](https://github.com/noskillcoding/pdata-client/blob/main/data/) | Samples of the nightly bulk files, CC BY 4.0 |
74
+ | [`examples/`](https://github.com/noskillcoding/pdata-client/blob/main/examples/) | Runnable scripts and an MCP client config |
75
+
76
+ ## What the client covers
77
+
78
+ | Method | API operation | What it returns |
79
+ |---|---|---|
80
+ | `markets(**filters)`, `iter_markets(...)` | `GET /markets` | Markets, filtered and sorted, one page or all pages |
81
+ | `market(source, id)` | `GET /markets/{source}/{id}` | One market |
82
+ | `top_movers(...)`, `closing_soon(...)` | `GET /markets/top-movers`, `/closing-soon` | Largest 24h price changes; markets closing next |
83
+ | `events(**filters)`, `iter_events(...)` | `GET /events` | Events (an event groups the markets of one question) |
84
+ | `event(source, id)` | `GET /events/{source}/{id}` | One event with its markets, or its final results once archived |
85
+ | `event_history(source, id, range=...)` | `GET /events/{source}/{id}/history` | Price and 24h-volume series, `24h` / `7d` / `30d` / `all` |
86
+ | `similar_events(source, id)` | `GET /events/{source}/{id}/similar` | The same or similar question on other venues |
87
+ | `search(q)` | `GET /search/suggest` | Events and markets by title |
88
+ | `summary()` | `GET /summary` | Per-venue counts and 24h volume |
89
+ | `news_movers(...)` | `GET /news/movers` | The price moves behind pdata.world/news |
90
+ | `bulk_index()`, `iter_bulk(kind)` | `/bulk/` | Nightly full files, streamed without saving |
91
+
92
+ Every method returns the API's JSON unchanged, so the
93
+ [API reference](https://api.pdata.world/docs) describes every field. Unknown filters are
94
+ rejected with a message listing every allowed one. Errors raise `PdataError` with the API's
95
+ `code` and `message`.
96
+
97
+ Runnable examples are in [`examples/`](https://github.com/noskillcoding/pdata-client/blob/main/examples/):
98
+ [`quickstart.py`](https://github.com/noskillcoding/pdata-client/blob/main/examples/quickstart.py) makes five common calls, and
99
+ [`resolved_archive.py`](https://github.com/noskillcoding/pdata-client/blob/main/examples/resolved_archive.py) streams the archive of settled events.
100
+
101
+ ## Other ways to get the same data
102
+
103
+ - **MCP server for AI assistants:** `https://api.pdata.world/mcp`; see
104
+ [above](#the-mcp-server-and-the-assistant-plugins).
105
+ - **Nightly bulk files** at <https://api.pdata.world/bulk/>: every open market
106
+ (`markets-open`), every open event (`events-open`) and the archive of settled events with
107
+ their results (`resolved`), as gzipped JSON lines. Line 1 of each file is a manifest
108
+ giving the licence and every field's meaning and unit.
109
+ - **Monthly snapshot on Hugging Face:** the same three files as Parquet, each snapshot
110
+ tagged with its date:
111
+ [`lowskillcoding/pdata-prediction-markets`](https://huggingface.co/datasets/lowskillcoding/pdata-prediction-markets).
112
+ - **Read-only Postgres** with full SQL:
113
+ `postgresql://pdata_readonly:pdata_public_2026@pg.pdata.world:6433/pdata_new`. The
114
+ password is published on purpose and is never rotated. Queries are capped at 5 seconds.
115
+ See [`examples/postgres.sql`](https://github.com/noskillcoding/pdata-client/blob/main/examples/postgres.sql).
116
+ - **The website:** every event has a page at `https://pdata.world/events/{source}/{id}`,
117
+ and <https://pdata.world/data> describes all of the above.
118
+
119
+ ## Sample data
120
+
121
+ [`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
122
+ open markets, 500 open events and 1,000 settled events, with an equal share per venue where
123
+ the venue had enough rows. Each file starts with the manifest line. They are for looking at
124
+ the shape of the data; the full files are rebuilt every night.
125
+
126
+ ## Units and things to know
127
+
128
+ - `probability` and prices are on a 0–1 scale.
129
+ - `volume` and `volume_24hr` are in each venue's own unit and are **not** converted across
130
+ venues: shares for Polymarket and contracts for Kalshi (each pays $1 at resolution),
131
+ play-money mana for Manifold, and each other venue's figure as it reports it.
132
+ [How eight venues count volume](https://pdata.world/research/prediction-market-volume)
133
+ explains each one.
134
+ - Price history starts in May 2026. Points older than 7 days are kept as 12-hour buckets,
135
+ and a market's history is deleted 10 days after it closes. After that, the event's URL
136
+ returns `"kind": "tombstone"` with each top market's final result.
137
+ - In the `resolved` archive, each market's `probability` is the last price pdata saw, which
138
+ is usually after the market settled. It records the result; it is not a forecast.
139
+ - Events that closed between 2 June and 11 August 2026 are missing from the `resolved`
140
+ archive: they were pruned before a backup was taken.
141
+ - pdata groups Kalshi's sibling events for one game under a single group event
142
+ (`is_group`); its volume is the sum of its children.
143
+
144
+ ## Research using this data
145
+
146
+ pdata.world publishes dated reports built from this data, each with its method and limits:
147
+
148
+ - [How accurate were prediction-market prices a day before the result?](https://pdata.world/research/prediction-market-accuracy)
149
+ For 39,681 Polymarket and Kalshi markets that settled between 28 September and
150
+ 5 October 2026, grouped into 10-point bands by their price 24 hours before close, the
151
+ share that resolved YES was on average 0.96 percentage points from the band's average
152
+ price (Brier score 0.110, against 0.199 for always guessing the base rate). Also published
153
+ as a citable note: [doi.org/10.5281/zenodo.23212130](https://doi.org/10.5281/zenodo.23212130).
154
+ - [How eight prediction-market venues count volume](https://pdata.world/research/prediction-market-volume)
155
+ - [What each prediction-market API gives you](https://pdata.world/research/prediction-market-apis)
156
+ - [Same question, different price: prediction markets across venues](https://pdata.world/research/cross-venue-prices)
157
+ - [What eight prediction-market venues list, by category](https://pdata.world/research/prediction-market-coverage)
158
+
159
+ ## Citing
160
+
161
+ The data is CC BY 4.0. A credit such as "Data: pdata.world" with a link is enough. Every
162
+ single-event API answer also carries `_meta.cite_as`, a ready-made citation sentence. For
163
+ papers, cite the dataset on Zenodo: DOI
164
+ [10.5281/zenodo.23211610](https://doi.org/10.5281/zenodo.23211610) always resolves to the
165
+ latest snapshot, and each snapshot has its own DOI, such as
166
+ [10.5281/zenodo.23211611](https://doi.org/10.5281/zenodo.23211611) for 7 October 2026. The
167
+ calibration note is [10.5281/zenodo.23212130](https://doi.org/10.5281/zenodo.23212130).
168
+ GitHub's "Cite this repository" button uses [`CITATION.cff`](https://github.com/noskillcoding/pdata-client/blob/main/CITATION.cff).
169
+
170
+ ## Licence
171
+
172
+ The code in this repository, including the plugin files and the Apify actor, is under the
173
+ [MIT licence](https://github.com/noskillcoding/pdata-client/blob/main/LICENSE). The data, including the files in `data/`, is under
174
+ [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
175
+ own terms.
176
+
177
+ pdata.world is independent and not affiliated with any of the venues it tracks.
178
+
179
+ ## Releasing
180
+
181
+ One version covers the PyPI package, the Claude Code plugin and the Gemini extension. Bump
182
+ it in [`src/pdata/_version.py`](https://github.com/noskillcoding/pdata-client/blob/main/src/pdata/_version.py),
183
+ [`.claude-plugin/plugin.json`](https://github.com/noskillcoding/pdata-client/blob/main/.claude-plugin/plugin.json) and
184
+ [`gemini-extension.json`](https://github.com/noskillcoding/pdata-client/blob/main/gemini-extension.json) together (a test fails otherwise), then
185
+ push a tag `v<version>`: [`publish.yml`](https://github.com/noskillcoding/pdata-client/blob/main/.github/workflows/publish.yml) builds, tests and
186
+ uploads to PyPI through trusted publishing. Contact: support@pdata.world.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pdata-client"
7
+ dynamic = ["version"]
8
+ description = "Python client for pdata.world, a free read-only API for prediction-market data from Polymarket, Kalshi, Manifold and five more venues."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "pdata.world", email = "support@pdata.world" }]
14
+ keywords = ["prediction markets", "polymarket", "kalshi", "manifold", "api client", "open data"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Office/Business :: Financial",
29
+ "Topic :: Scientific/Engineering :: Information Analysis",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = []
33
+
34
+ [project.urls]
35
+ Homepage = "https://pdata.world"
36
+ Documentation = "https://api.pdata.world/docs"
37
+ "OpenAPI" = "https://api.pdata.world/openapi.json"
38
+ Repository = "https://github.com/noskillcoding/pdata-client"
39
+ Issues = "https://github.com/noskillcoding/pdata-client/issues"
40
+ Data = "https://pdata.world/data"
41
+ Research = "https://pdata.world/research"
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = { attr = "pdata._version.__version__" }
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ pdata = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -0,0 +1,3 @@
1
+ """The package version, in one place (pyproject.toml reads it from here)."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
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,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pdata/__init__.py
5
+ src/pdata/_version.py
6
+ src/pdata/client.py
7
+ src/pdata/py.typed
8
+ src/pdata_client.egg-info/PKG-INFO
9
+ src/pdata_client.egg-info/SOURCES.txt
10
+ src/pdata_client.egg-info/dependency_links.txt
11
+ src/pdata_client.egg-info/top_level.txt
12
+ tests/test_client.py
13
+ tests/test_manifests.py
@@ -0,0 +1,149 @@
1
+ """Offline tests: a fake opener stands in for the network.
2
+
3
+ Set PDATA_LIVE=1 to also run the live smoke test against api.pdata.world.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import gzip
9
+ import io
10
+ import json
11
+ import os
12
+ import unittest
13
+ from urllib.error import HTTPError
14
+ from urllib.parse import parse_qs, urlsplit
15
+
16
+ import pdata
17
+ from pdata import Client, PdataError
18
+
19
+
20
+ class FakeResponse(io.BytesIO):
21
+ def __enter__(self):
22
+ return self
23
+
24
+ def __exit__(self, *exc):
25
+ self.close()
26
+ return False
27
+
28
+
29
+ class FakeOpener:
30
+ """Answers each URL path from a dict; records every request."""
31
+
32
+ def __init__(self, routes):
33
+ self.routes = routes
34
+ self.urls = []
35
+ self.requests = []
36
+
37
+ def __call__(self, request, timeout=None):
38
+ url = request.full_url
39
+ self.urls.append(url)
40
+ self.requests.append(request)
41
+ answer = self.routes[urlsplit(url).path]
42
+ if callable(answer):
43
+ answer = answer(url)
44
+ if isinstance(answer, HTTPError):
45
+ raise answer
46
+ if isinstance(answer, bytes):
47
+ return FakeResponse(answer)
48
+ return FakeResponse(json.dumps(answer).encode())
49
+
50
+
51
+ def page(items, page_no, total_pages):
52
+ return {"items": items, "meta": {"page": page_no, "total_pages": total_pages}}
53
+
54
+
55
+ def query(url):
56
+ return {k: v[0] for k, v in parse_qs(urlsplit(url).query).items()}
57
+
58
+
59
+ class ClientTests(unittest.TestCase):
60
+ def test_params_are_encoded(self):
61
+ opener = FakeOpener({"/api/v1/markets": page([], 1, 1)})
62
+ Client(opener=opener).markets(
63
+ source=["kalshi", "polymarket"], closed=False, search=None, sort="-volume_24hr"
64
+ )
65
+ q = query(opener.urls[0])
66
+ self.assertEqual(q["source"], "kalshi,polymarket")
67
+ self.assertEqual(q["closed"], "false")
68
+ self.assertEqual(q["sort"], "-volume_24hr")
69
+ self.assertNotIn("search", q)
70
+
71
+ def test_default_user_agent_names_client_version_and_agents_page(self):
72
+ opener = FakeOpener({"/api/v1/summary": {}})
73
+ Client(opener=opener).summary()
74
+ Client(opener=opener, user_agent="my-project/2.0").summary()
75
+ sent = [r.get_header("User-agent") for r in opener.requests]
76
+ self.assertEqual(
77
+ sent[0], f"pdata-client/{pdata.__version__} (+https://pdata.world/agents)"
78
+ )
79
+ self.assertEqual(sent[1], "my-project/2.0")
80
+
81
+ def test_iter_markets_walks_pages_and_stops_at_max_items(self):
82
+ def answer(url):
83
+ n = int(query(url)["page"])
84
+ return page([{"id": f"{n}-{i}"} for i in range(2)], n, 3)
85
+
86
+ opener = FakeOpener({"/api/v1/markets": answer})
87
+ client = Client(opener=opener)
88
+ ids = [m["id"] for m in client.iter_markets(source="kalshi")]
89
+ self.assertEqual(ids, ["1-0", "1-1", "2-0", "2-1", "3-0", "3-1"])
90
+ self.assertEqual(query(opener.urls[0])["page_size"], "200")
91
+
92
+ opener.urls.clear()
93
+ self.assertEqual(len(list(client.iter_markets(max_items=3))), 3)
94
+ self.assertEqual(len(opener.urls), 2)
95
+
96
+ def test_ids_are_escaped_as_one_path_segment(self):
97
+ opener = FakeOpener({"/api/v1/events/kalshi/A%2FB/history": {"series": []}})
98
+ Client(opener=opener).event_history("kalshi", "A/B", range="7d")
99
+ self.assertEqual(query(opener.urls[0]), {"range": "7d"})
100
+
101
+ def test_api_error_becomes_pdata_error(self):
102
+ body = json.dumps({"code": "not_found", "message": "Event not found: kalshi/X"})
103
+ err = HTTPError("u", 404, "Not Found", {}, io.BytesIO(body.encode()))
104
+ opener = FakeOpener({"/api/v1/events/kalshi/X": err})
105
+ with self.assertRaises(PdataError) as caught:
106
+ Client(opener=opener).event("kalshi", "X")
107
+ self.assertEqual(caught.exception.status, 404)
108
+ self.assertEqual(caught.exception.code, "not_found")
109
+
110
+ def test_retries_a_503_then_succeeds(self):
111
+ calls = []
112
+
113
+ def answer(url):
114
+ calls.append(url)
115
+ if len(calls) == 1:
116
+ return HTTPError(url, 503, "Unavailable", {"Retry-After": "0"}, io.BytesIO(b""))
117
+ return {"ok": True}
118
+
119
+ opener = FakeOpener({"/api/v1/summary": answer})
120
+ self.assertEqual(Client(opener=opener).summary(), {"ok": True})
121
+ self.assertEqual(len(calls), 2)
122
+
123
+ def test_iter_bulk_skips_the_manifest_line(self):
124
+ lines = [{"_manifest": {"license": "CC BY 4.0"}}, {"id": "a"}, {"id": "b"}]
125
+ blob = gzip.compress("\n".join(json.dumps(x) for x in lines).encode())
126
+ opener = FakeOpener({"/bulk/resolved-latest.jsonl.gz": blob})
127
+ client = Client(opener=opener)
128
+ self.assertEqual([r["id"] for r in client.iter_bulk("resolved")], ["a", "b"])
129
+ first = next(client.iter_bulk("resolved", include_manifest=True))
130
+ self.assertEqual(first["_manifest"]["license"], "CC BY 4.0")
131
+ with self.assertRaises(ValueError):
132
+ list(client.iter_bulk("everything"))
133
+
134
+
135
+ @unittest.skipUnless(os.environ.get("PDATA_LIVE"), "set PDATA_LIVE=1 to hit the live API")
136
+ class LiveSmokeTest(unittest.TestCase):
137
+ def test_live_endpoints_answer(self):
138
+ client = Client()
139
+ top = client.markets(sort="-volume_24hr", page_size=3)
140
+ self.assertEqual(len(top["items"]), 3)
141
+ first = top["items"][0]
142
+ event = client.event(first["source"], first["event_id"], markets_limit=1)
143
+ self.assertIn(event["kind"], ("live", "tombstone"))
144
+ self.assertTrue(client.summary())
145
+ self.assertIn("files", client.bulk_index())
146
+
147
+
148
+ if __name__ == "__main__":
149
+ unittest.main()
@@ -0,0 +1,54 @@
1
+ """The plugin manifests stay in step with the Python package.
2
+
3
+ One version for the whole repository: the PyPI package, the Claude Code
4
+ plugin and the Gemini CLI extension. Bump them together; the publish
5
+ workflow runs this test before it uploads anything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ MCP_URL = "https://api.pdata.world/mcp"
17
+
18
+
19
+ def _load(relative: str):
20
+ return json.loads((ROOT / relative).read_text(encoding="utf-8"))
21
+
22
+
23
+ @unittest.skipUnless(
24
+ (ROOT / ".claude-plugin" / "plugin.json").exists(), "plugin files are not in this copy"
25
+ )
26
+ class ManifestTests(unittest.TestCase):
27
+ def setUp(self):
28
+ text = (ROOT / "src" / "pdata" / "_version.py").read_text(encoding="utf-8")
29
+ self.version = re.search(r'__version__ = "([^"]+)"', text).group(1)
30
+
31
+ def test_versions_match_the_package(self):
32
+ self.assertEqual(_load(".claude-plugin/plugin.json")["version"], self.version)
33
+ self.assertEqual(_load("gemini-extension.json")["version"], self.version)
34
+
35
+ def test_every_manifest_points_at_the_hosted_server(self):
36
+ claude = _load(".mcp.json")["mcpServers"]["pdata"]
37
+ gemini = _load("gemini-extension.json")["mcpServers"]["pdata"]
38
+ self.assertEqual((claude["type"], claude["url"]), ("http", MCP_URL))
39
+ self.assertEqual((gemini["type"], gemini["url"]), ("http", MCP_URL))
40
+
41
+ def test_names_line_up(self):
42
+ plugin = _load(".claude-plugin/plugin.json")["name"]
43
+ entry = _load(".claude-plugin/marketplace.json")["plugins"][0]
44
+ self.assertEqual(entry["name"], plugin)
45
+ self.assertEqual(entry["source"], "./")
46
+ self.assertEqual(_load("gemini-extension.json")["name"], plugin)
47
+ skill = (ROOT / "skills" / "pdata-prediction-markets" / "SKILL.md").read_text(
48
+ encoding="utf-8"
49
+ )
50
+ self.assertTrue(skill.startswith("---\nname: pdata-prediction-markets\n"))
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()