clause-mcp 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.
- clause_mcp-0.1.0/LICENSE +21 -0
- clause_mcp-0.1.0/PKG-INFO +199 -0
- clause_mcp-0.1.0/README.md +160 -0
- clause_mcp-0.1.0/clause/__init__.py +0 -0
- clause_mcp-0.1.0/clause/adapters/__init__.py +13 -0
- clause_mcp-0.1.0/clause/adapters/base.py +203 -0
- clause_mcp-0.1.0/clause/adapters/kalshi.py +118 -0
- clause_mcp-0.1.0/clause/adapters/limitless.py +229 -0
- clause_mcp-0.1.0/clause/adapters/polymarket.py +117 -0
- clause_mcp-0.1.0/clause/adapters/predictit.py +175 -0
- clause_mcp-0.1.0/clause/agents/__init__.py +7 -0
- clause_mcp-0.1.0/clause/agents/discovery.py +253 -0
- clause_mcp-0.1.0/clause/agents/markdown.py +247 -0
- clause_mcp-0.1.0/clause/agents/middleware.py +83 -0
- clause_mcp-0.1.0/clause/agents/routes.py +72 -0
- clause_mcp-0.1.0/clause/alerts/__init__.py +0 -0
- clause_mcp-0.1.0/clause/alerts/deliver.py +73 -0
- clause_mcp-0.1.0/clause/alerts/detect.py +198 -0
- clause_mcp-0.1.0/clause/alerts/rules.py +299 -0
- clause_mcp-0.1.0/clause/alerts/telegram.py +144 -0
- clause_mcp-0.1.0/clause/analytics/__init__.py +0 -0
- clause_mcp-0.1.0/clause/analytics/execution.py +268 -0
- clause_mcp-0.1.0/clause/analytics/zero_copy.py +202 -0
- clause_mcp-0.1.0/clause/api/__init__.py +0 -0
- clause_mcp-0.1.0/clause/api/auth.py +182 -0
- clause_mcp-0.1.0/clause/api/crossvenue_routes.py +196 -0
- clause_mcp-0.1.0/clause/api/execution_routes.py +1039 -0
- clause_mcp-0.1.0/clause/api/keys_middleware.py +83 -0
- clause_mcp-0.1.0/clause/api/main.py +422 -0
- clause_mcp-0.1.0/clause/archive/__init__.py +0 -0
- clause_mcp-0.1.0/clause/archive/recorder.py +320 -0
- clause_mcp-0.1.0/clause/archive/verify.py +186 -0
- clause_mcp-0.1.0/clause/chain/__init__.py +0 -0
- clause_mcp-0.1.0/clause/chain/adapters.py +92 -0
- clause_mcp-0.1.0/clause/chain/events.py +48 -0
- clause_mcp-0.1.0/clause/chain/rpc.py +185 -0
- clause_mcp-0.1.0/clause/content/__init__.py +1 -0
- clause_mcp-0.1.0/clause/content/findings.py +464 -0
- clause_mcp-0.1.0/clause/content/formats.py +190 -0
- clause_mcp-0.1.0/clause/content/longform.py +409 -0
- clause_mcp-0.1.0/clause/content/pieces/__init__.py +35 -0
- clause_mcp-0.1.0/clause/content/pieces/eighty_four_cent_outcome.py +186 -0
- clause_mcp-0.1.0/clause/content/publish.py +196 -0
- clause_mcp-0.1.0/clause/content/render.py +179 -0
- clause_mcp-0.1.0/clause/evidence/__init__.py +0 -0
- clause_mcp-0.1.0/clause/evidence/bundle.py +198 -0
- clause_mcp-0.1.0/clause/evidence/scorecard.py +185 -0
- clause_mcp-0.1.0/clause/execution/__init__.py +0 -0
- clause_mcp-0.1.0/clause/execution/capacity.py +399 -0
- clause_mcp-0.1.0/clause/execution/consolidated.py +194 -0
- clause_mcp-0.1.0/clause/execution/copy.py +516 -0
- clause_mcp-0.1.0/clause/execution/edge.py +205 -0
- clause_mcp-0.1.0/clause/execution/liquidity.py +317 -0
- clause_mcp-0.1.0/clause/execution/markout.py +208 -0
- clause_mcp-0.1.0/clause/execution/orders.py +297 -0
- clause_mcp-0.1.0/clause/execution/passive.py +263 -0
- clause_mcp-0.1.0/clause/execution/risk.py +246 -0
- clause_mcp-0.1.0/clause/execution/router.py +234 -0
- clause_mcp-0.1.0/clause/execution/schedule.py +292 -0
- clause_mcp-0.1.0/clause/execution/scorecard.py +203 -0
- clause_mcp-0.1.0/clause/execution/screen.py +168 -0
- clause_mcp-0.1.0/clause/execution/strategy.py +223 -0
- clause_mcp-0.1.0/clause/ingest/__init__.py +0 -0
- clause_mcp-0.1.0/clause/ingest/stream.py +228 -0
- clause_mcp-0.1.0/clause/matching/__init__.py +0 -0
- clause_mcp-0.1.0/clause/matching/canonical.py +123 -0
- clause_mcp-0.1.0/clause/matching/criteria_match.py +493 -0
- clause_mcp-0.1.0/clause/matching/crossvenue.py +242 -0
- clause_mcp-0.1.0/clause/matching/equivalence.py +267 -0
- clause_mcp-0.1.0/clause/mcp/__init__.py +0 -0
- clause_mcp-0.1.0/clause/mcp/server.py +651 -0
- clause_mcp-0.1.0/clause/net/__init__.py +0 -0
- clause_mcp-0.1.0/clause/net/budget.py +141 -0
- clause_mcp-0.1.0/clause/net/cache.py +99 -0
- clause_mcp-0.1.0/clause/net/ratelimit.py +88 -0
- clause_mcp-0.1.0/clause/polymarket/__init__.py +0 -0
- clause_mcp-0.1.0/clause/polymarket/client.py +308 -0
- clause_mcp-0.1.0/clause/resolution/__init__.py +0 -0
- clause_mcp-0.1.0/clause/resolution/lifecycle.py +271 -0
- clause_mcp-0.1.0/clause/resolution/schema.py +243 -0
- clause_mcp-0.1.0/clause/resolution/store.py +351 -0
- clause_mcp-0.1.0/clause/resolution/tracker.py +400 -0
- clause_mcp-0.1.0/clause/search/__init__.py +15 -0
- clause_mcp-0.1.0/clause/search/markets.py +428 -0
- clause_mcp-0.1.0/clause/service/__init__.py +0 -0
- clause_mcp-0.1.0/clause/service/execution_service.py +387 -0
- clause_mcp-0.1.0/clause/service/wallet_report.py +223 -0
- clause_mcp-0.1.0/clause/store/__init__.py +0 -0
- clause_mcp-0.1.0/clause/store/backend.py +210 -0
- clause_mcp-0.1.0/clause/store/db.py +408 -0
- clause_mcp-0.1.0/clause/store/joblock.py +205 -0
- clause_mcp-0.1.0/clause/store/prices.py +128 -0
- clause_mcp-0.1.0/clause/store/schema.sql +188 -0
- clause_mcp-0.1.0/clause/terms/__init__.py +0 -0
- clause_mcp-0.1.0/clause/terms/criteria.py +253 -0
- clause_mcp-0.1.0/clause/terms/extract.py +283 -0
- clause_mcp-0.1.0/clause/terms/normalise.py +80 -0
- clause_mcp-0.1.0/clause/tools/__init__.py +0 -0
- clause_mcp-0.1.0/clause/tools/explain.py +200 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/PKG-INFO +199 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/SOURCES.txt +139 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/dependency_links.txt +1 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/entry_points.txt +2 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/requires.txt +23 -0
- clause_mcp-0.1.0/clause_mcp.egg-info/top_level.txt +1 -0
- clause_mcp-0.1.0/pyproject.toml +63 -0
- clause_mcp-0.1.0/setup.cfg +4 -0
- clause_mcp-0.1.0/tests/test_across_route.py +148 -0
- clause_mcp-0.1.0/tests/test_agent_discovery.py +207 -0
- clause_mcp-0.1.0/tests/test_alerts.py +351 -0
- clause_mcp-0.1.0/tests/test_archive_absent.py +259 -0
- clause_mcp-0.1.0/tests/test_archive_memory.py +110 -0
- clause_mcp-0.1.0/tests/test_archive_verify.py +156 -0
- clause_mcp-0.1.0/tests/test_backup.py +115 -0
- clause_mcp-0.1.0/tests/test_client.py +81 -0
- clause_mcp-0.1.0/tests/test_copy.py +288 -0
- clause_mcp-0.1.0/tests/test_crossvenue.py +203 -0
- clause_mcp-0.1.0/tests/test_deploy_units.py +62 -0
- clause_mcp-0.1.0/tests/test_evidence.py +167 -0
- clause_mcp-0.1.0/tests/test_explain_route.py +333 -0
- clause_mcp-0.1.0/tests/test_findings.py +158 -0
- clause_mcp-0.1.0/tests/test_follow_costs.py +152 -0
- clause_mcp-0.1.0/tests/test_formats.py +251 -0
- clause_mcp-0.1.0/tests/test_golden.py +1449 -0
- clause_mcp-0.1.0/tests/test_health.py +72 -0
- clause_mcp-0.1.0/tests/test_joblock.py +321 -0
- clause_mcp-0.1.0/tests/test_kalshi_book.py +112 -0
- clause_mcp-0.1.0/tests/test_limitless_authorisation.py +95 -0
- clause_mcp-0.1.0/tests/test_longform.py +226 -0
- clause_mcp-0.1.0/tests/test_markout.py +119 -0
- clause_mcp-0.1.0/tests/test_novig.py +150 -0
- clause_mcp-0.1.0/tests/test_pieces.py +64 -0
- clause_mcp-0.1.0/tests/test_predictit.py +123 -0
- clause_mcp-0.1.0/tests/test_publish.py +162 -0
- clause_mcp-0.1.0/tests/test_publishable.py +117 -0
- clause_mcp-0.1.0/tests/test_resolution.py +816 -0
- clause_mcp-0.1.0/tests/test_scale.py +173 -0
- clause_mcp-0.1.0/tests/test_scheduling.py +137 -0
- clause_mcp-0.1.0/tests/test_search.py +196 -0
- clause_mcp-0.1.0/tests/test_tools.py +85 -0
- clause_mcp-0.1.0/tests/test_wallet_analysis.py +200 -0
clause_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clause
|
|
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,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clause-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for prediction markets: are these two contracts the same bet, and how much size will this market actually take?
|
|
5
|
+
Author: Clause
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: mcp,model-context-protocol,prediction-markets,polymarket,kalshi,order-book,liquidity,market-microstructure
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: httpx>=0.28
|
|
21
|
+
Requires-Dist: mcp>=2.0
|
|
22
|
+
Provides-Extra: api
|
|
23
|
+
Requires-Dist: fastapi>=0.115; extra == "api"
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == "api"
|
|
25
|
+
Provides-Extra: chain
|
|
26
|
+
Requires-Dist: eth-abi>=5.0; extra == "chain"
|
|
27
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.7; extra == "chain"
|
|
28
|
+
Requires-Dist: websockets>=15; extra == "chain"
|
|
29
|
+
Provides-Extra: postgres
|
|
30
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
|
|
31
|
+
Provides-Extra: analysis
|
|
32
|
+
Requires-Dist: duckdb>=1.5; extra == "analysis"
|
|
33
|
+
Requires-Dist: rapidfuzz>=3.14; extra == "analysis"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
37
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# clause-mcp
|
|
41
|
+
|
|
42
|
+
**You are about to trade a spread between two contracts that resolve
|
|
43
|
+
differently, and nothing you own will tell you.**
|
|
44
|
+
|
|
45
|
+
Polymarket lists *"U.S. enacts AI safety bill before 2027?"*. Kalshi lists what
|
|
46
|
+
reads as the same contract. Polymarket's resolves YES only on **restrictive
|
|
47
|
+
provisions**. Kalshi's resolves YES on a federal framework *"regardless of
|
|
48
|
+
whether those provisions mandate private-sector compliance or **promote
|
|
49
|
+
voluntary guidelines**."*
|
|
50
|
+
|
|
51
|
+
A voluntary standards act resolves one YES and the other NO. They are not the
|
|
52
|
+
same bet at any price. Every arbitrage screen in existence shows them as one
|
|
53
|
+
market at a spread, and a language model reading both rules texts will agree
|
|
54
|
+
with the screen — because the divergence is not a difference between the two
|
|
55
|
+
documents. It is **an explicit clause on one side matched by silence on the
|
|
56
|
+
other**, and silence is the one thing a comparator has to be told not to read
|
|
57
|
+
as agreement.
|
|
58
|
+
|
|
59
|
+
This is an MCP server. It gives an agent the checks it cannot perform on its
|
|
60
|
+
own.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## The second failure: the book that isn't there
|
|
65
|
+
|
|
66
|
+
A market on a major venue reported **$65,883** of order-book depth. Within a
|
|
67
|
+
sensible band of its own mid it held **$33**. It had traded **$122 in thirty
|
|
68
|
+
days**. Putting $200,000 into it is 49,276 days of volume — **135 years**.
|
|
69
|
+
|
|
70
|
+
A book is an offer. The tape is a fact. When they disagree, believe the tape.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install clause-mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Claude Code
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
claude mcp add --scope user clause -- clause-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--scope user` matters: the default registers the server for one project
|
|
87
|
+
directory only. **The tools appear in a new session**, not the one you ran the
|
|
88
|
+
add from.
|
|
89
|
+
|
|
90
|
+
### Claude Desktop, or any stdio client
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"clause": { "command": "clause-mcp" }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Streamable-HTTP instead of stdio: `clause-mcp --http --port 8848`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The eight tools
|
|
105
|
+
|
|
106
|
+
| Tool | Answers | Needs |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `check_equivalence` | are these two contracts the same bet? | live venues |
|
|
109
|
+
| `market_liquidity` | can this market absorb size — book *and* tape | live venues |
|
|
110
|
+
| `preflight` | will this specific order work, with numbers | live venues |
|
|
111
|
+
| `capacity` | how much fits inside 0.5¢ / 1¢ / 2¢ / 5¢ | live venues |
|
|
112
|
+
| `resting_order` | rest or cross, and how long to fill | live venues |
|
|
113
|
+
| `venue_conventions` | how each venue's API lies to you | nothing |
|
|
114
|
+
| `find_markets` | identifiers the other tools need | an archive |
|
|
115
|
+
| `identify_market` | which contract did the user mean, in plain words | an archive |
|
|
116
|
+
|
|
117
|
+
**Six of the eight work the moment you install this.** They read the venues
|
|
118
|
+
live and need no local data.
|
|
119
|
+
|
|
120
|
+
The two search tools need a Clause archive — a hash-chained daily record of
|
|
121
|
+
every open market's terms. This package does not ship one, and it will not
|
|
122
|
+
pretend otherwise: with no archive, both tools **refuse**, name the state, and
|
|
123
|
+
say in words that nothing was searched.
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"ok": false,
|
|
128
|
+
"error": "archive_unavailable",
|
|
129
|
+
"this_is_not_a_negative_result":
|
|
130
|
+
"Do NOT report this as 'no such market' or 'no results'. Nothing was
|
|
131
|
+
searched. The named market may exist and be trading; this install
|
|
132
|
+
simply has no corpus to look in."
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
That refusal is the point of the package rather than a limitation of it. The
|
|
137
|
+
alternative — falling back to a live search — is what the tool exists to
|
|
138
|
+
avoid: Polymarket's list API caps pages at 100 rows and rejects offsets past
|
|
139
|
+
~2,100, so a live search silently misses the quiet markets, which are exactly
|
|
140
|
+
the ones whose liquidity most needs checking. Point `CLAUSE_DB` at an archive
|
|
141
|
+
if you have one.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Why the tool descriptions read the way they do
|
|
146
|
+
|
|
147
|
+
They are written for the agent, not for a human browsing a catalogue. An agent
|
|
148
|
+
does not know it has a problem, so each description leads with the failure it
|
|
149
|
+
prevents rather than with what the tool returns. A tool called after the
|
|
150
|
+
mistake has been made is a tool that did nothing.
|
|
151
|
+
|
|
152
|
+
The verdicts are built the same way. `check_equivalence` returns two of them,
|
|
153
|
+
because two different things can make two contracts different bets. `verdict`
|
|
154
|
+
compares thresholds and settlement sources; it can reach `EQUIVALENT` when
|
|
155
|
+
both are genuinely comparable. `criteria_verdict` compares the *provisions* —
|
|
156
|
+
subject, jurisdiction, actor, action breadth, voluntary conduct, logical
|
|
157
|
+
structure, carve-outs — and it **never returns a bare `EQUIVALENT`**:
|
|
158
|
+
`LIKELY_EQUIVALENT` is its ceiling, with the clauses to read attached. That is
|
|
159
|
+
the one that catches the AI bills above, and roughly half the board resolves
|
|
160
|
+
on provisions rather than on numbers.
|
|
161
|
+
|
|
162
|
+
An `unknown` on a material field returns `REVIEW_REQUIRED`, never a pass.
|
|
163
|
+
`INSUFFICIENT_DATA` is a distinct verdict from `LIQUID` and must never render
|
|
164
|
+
as one.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `venue_conventions`
|
|
169
|
+
|
|
170
|
+
Thirteen documented ways a venue API returns HTTP 200 while being wrong, each
|
|
171
|
+
one found by losing time to it and each one pinned by a test. A sample:
|
|
172
|
+
|
|
173
|
+
- Polymarket's `/book` returns **asks descending** — `asks[0]` is the worst price.
|
|
174
|
+
- A **YES bid at 0.10 is a NO ask at 0.90**. One order, two books. Never add the ladders.
|
|
175
|
+
- `data-api /trades` **ignores the `asset` parameter** and returns the full tape. A five-row sample "confirms" the filter works.
|
|
176
|
+
- Trades are **newest-first**; a capped pull is recent history, so dividing by 30 days understates a busy market ~15×.
|
|
177
|
+
- Kalshi's orderbook is under `orderbook_fp`, and **both sides are resting bids** — the NO ask is `1 − best YES bid`.
|
|
178
|
+
- Limitless caps `limit` at 25 and **400s above it**, which reads as "0 markets found".
|
|
179
|
+
|
|
180
|
+
The meta-lesson, and the reason the tools refuse the way they do: **a check
|
|
181
|
+
that cannot run reads as a check that passed.**
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Honest status
|
|
186
|
+
|
|
187
|
+
Version 0.1.0. The verdicts are conservative by construction and the refusals
|
|
188
|
+
are deliberate, but this is early software with no user base behind it yet.
|
|
189
|
+
It measures and records; **it does not recommend positions and it is not legal
|
|
190
|
+
advice**. Order transmission is off.
|
|
191
|
+
|
|
192
|
+
Requires Python 3.11+. Tested on 3.13.
|
|
193
|
+
|
|
194
|
+
**Name collision:** an unrelated PyPI package called `clause` (a Chinese NLP
|
|
195
|
+
SDK) also installs a top-level `clause` module. Installing both into one
|
|
196
|
+
environment will produce a broken hybrid. Run this server with `uvx`, `pipx`,
|
|
197
|
+
or its own virtualenv — which is how MCP servers are usually run anyway.
|
|
198
|
+
|
|
199
|
+
MIT licensed.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# clause-mcp
|
|
2
|
+
|
|
3
|
+
**You are about to trade a spread between two contracts that resolve
|
|
4
|
+
differently, and nothing you own will tell you.**
|
|
5
|
+
|
|
6
|
+
Polymarket lists *"U.S. enacts AI safety bill before 2027?"*. Kalshi lists what
|
|
7
|
+
reads as the same contract. Polymarket's resolves YES only on **restrictive
|
|
8
|
+
provisions**. Kalshi's resolves YES on a federal framework *"regardless of
|
|
9
|
+
whether those provisions mandate private-sector compliance or **promote
|
|
10
|
+
voluntary guidelines**."*
|
|
11
|
+
|
|
12
|
+
A voluntary standards act resolves one YES and the other NO. They are not the
|
|
13
|
+
same bet at any price. Every arbitrage screen in existence shows them as one
|
|
14
|
+
market at a spread, and a language model reading both rules texts will agree
|
|
15
|
+
with the screen — because the divergence is not a difference between the two
|
|
16
|
+
documents. It is **an explicit clause on one side matched by silence on the
|
|
17
|
+
other**, and silence is the one thing a comparator has to be told not to read
|
|
18
|
+
as agreement.
|
|
19
|
+
|
|
20
|
+
This is an MCP server. It gives an agent the checks it cannot perform on its
|
|
21
|
+
own.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## The second failure: the book that isn't there
|
|
26
|
+
|
|
27
|
+
A market on a major venue reported **$65,883** of order-book depth. Within a
|
|
28
|
+
sensible band of its own mid it held **$33**. It had traded **$122 in thirty
|
|
29
|
+
days**. Putting $200,000 into it is 49,276 days of volume — **135 years**.
|
|
30
|
+
|
|
31
|
+
A book is an offer. The tape is a fact. When they disagree, believe the tape.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install clause-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Claude Code
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
claude mcp add --scope user clause -- clause-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--scope user` matters: the default registers the server for one project
|
|
48
|
+
directory only. **The tools appear in a new session**, not the one you ran the
|
|
49
|
+
add from.
|
|
50
|
+
|
|
51
|
+
### Claude Desktop, or any stdio client
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"clause": { "command": "clause-mcp" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Streamable-HTTP instead of stdio: `clause-mcp --http --port 8848`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## The eight tools
|
|
66
|
+
|
|
67
|
+
| Tool | Answers | Needs |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `check_equivalence` | are these two contracts the same bet? | live venues |
|
|
70
|
+
| `market_liquidity` | can this market absorb size — book *and* tape | live venues |
|
|
71
|
+
| `preflight` | will this specific order work, with numbers | live venues |
|
|
72
|
+
| `capacity` | how much fits inside 0.5¢ / 1¢ / 2¢ / 5¢ | live venues |
|
|
73
|
+
| `resting_order` | rest or cross, and how long to fill | live venues |
|
|
74
|
+
| `venue_conventions` | how each venue's API lies to you | nothing |
|
|
75
|
+
| `find_markets` | identifiers the other tools need | an archive |
|
|
76
|
+
| `identify_market` | which contract did the user mean, in plain words | an archive |
|
|
77
|
+
|
|
78
|
+
**Six of the eight work the moment you install this.** They read the venues
|
|
79
|
+
live and need no local data.
|
|
80
|
+
|
|
81
|
+
The two search tools need a Clause archive — a hash-chained daily record of
|
|
82
|
+
every open market's terms. This package does not ship one, and it will not
|
|
83
|
+
pretend otherwise: with no archive, both tools **refuse**, name the state, and
|
|
84
|
+
say in words that nothing was searched.
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"ok": false,
|
|
89
|
+
"error": "archive_unavailable",
|
|
90
|
+
"this_is_not_a_negative_result":
|
|
91
|
+
"Do NOT report this as 'no such market' or 'no results'. Nothing was
|
|
92
|
+
searched. The named market may exist and be trading; this install
|
|
93
|
+
simply has no corpus to look in."
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
That refusal is the point of the package rather than a limitation of it. The
|
|
98
|
+
alternative — falling back to a live search — is what the tool exists to
|
|
99
|
+
avoid: Polymarket's list API caps pages at 100 rows and rejects offsets past
|
|
100
|
+
~2,100, so a live search silently misses the quiet markets, which are exactly
|
|
101
|
+
the ones whose liquidity most needs checking. Point `CLAUSE_DB` at an archive
|
|
102
|
+
if you have one.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Why the tool descriptions read the way they do
|
|
107
|
+
|
|
108
|
+
They are written for the agent, not for a human browsing a catalogue. An agent
|
|
109
|
+
does not know it has a problem, so each description leads with the failure it
|
|
110
|
+
prevents rather than with what the tool returns. A tool called after the
|
|
111
|
+
mistake has been made is a tool that did nothing.
|
|
112
|
+
|
|
113
|
+
The verdicts are built the same way. `check_equivalence` returns two of them,
|
|
114
|
+
because two different things can make two contracts different bets. `verdict`
|
|
115
|
+
compares thresholds and settlement sources; it can reach `EQUIVALENT` when
|
|
116
|
+
both are genuinely comparable. `criteria_verdict` compares the *provisions* —
|
|
117
|
+
subject, jurisdiction, actor, action breadth, voluntary conduct, logical
|
|
118
|
+
structure, carve-outs — and it **never returns a bare `EQUIVALENT`**:
|
|
119
|
+
`LIKELY_EQUIVALENT` is its ceiling, with the clauses to read attached. That is
|
|
120
|
+
the one that catches the AI bills above, and roughly half the board resolves
|
|
121
|
+
on provisions rather than on numbers.
|
|
122
|
+
|
|
123
|
+
An `unknown` on a material field returns `REVIEW_REQUIRED`, never a pass.
|
|
124
|
+
`INSUFFICIENT_DATA` is a distinct verdict from `LIQUID` and must never render
|
|
125
|
+
as one.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## `venue_conventions`
|
|
130
|
+
|
|
131
|
+
Thirteen documented ways a venue API returns HTTP 200 while being wrong, each
|
|
132
|
+
one found by losing time to it and each one pinned by a test. A sample:
|
|
133
|
+
|
|
134
|
+
- Polymarket's `/book` returns **asks descending** — `asks[0]` is the worst price.
|
|
135
|
+
- A **YES bid at 0.10 is a NO ask at 0.90**. One order, two books. Never add the ladders.
|
|
136
|
+
- `data-api /trades` **ignores the `asset` parameter** and returns the full tape. A five-row sample "confirms" the filter works.
|
|
137
|
+
- Trades are **newest-first**; a capped pull is recent history, so dividing by 30 days understates a busy market ~15×.
|
|
138
|
+
- Kalshi's orderbook is under `orderbook_fp`, and **both sides are resting bids** — the NO ask is `1 − best YES bid`.
|
|
139
|
+
- Limitless caps `limit` at 25 and **400s above it**, which reads as "0 markets found".
|
|
140
|
+
|
|
141
|
+
The meta-lesson, and the reason the tools refuse the way they do: **a check
|
|
142
|
+
that cannot run reads as a check that passed.**
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Honest status
|
|
147
|
+
|
|
148
|
+
Version 0.1.0. The verdicts are conservative by construction and the refusals
|
|
149
|
+
are deliberate, but this is early software with no user base behind it yet.
|
|
150
|
+
It measures and records; **it does not recommend positions and it is not legal
|
|
151
|
+
advice**. Order transmission is off.
|
|
152
|
+
|
|
153
|
+
Requires Python 3.11+. Tested on 3.13.
|
|
154
|
+
|
|
155
|
+
**Name collision:** an unrelated PyPI package called `clause` (a Chinese NLP
|
|
156
|
+
SDK) also installs a top-level `clause` module. Installing both into one
|
|
157
|
+
environment will produce a broken hybrid. Run this server with `uvx`, `pipx`,
|
|
158
|
+
or its own virtualenv — which is how MCP servers are usually run anyway.
|
|
159
|
+
|
|
160
|
+
MIT licensed.
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Venue adapters.
|
|
2
|
+
|
|
3
|
+
Importing this package registers every adapter. The registry is populated by
|
|
4
|
+
`@REGISTRY.register` at class definition, so it only knows the venues whose
|
|
5
|
+
modules something has imported -- which made `REGISTRY.storable()` return
|
|
6
|
+
whatever the importer happened to touch. The cross-venue matcher asked it for
|
|
7
|
+
the list of venues it may write, got a partial answer, and would have refused
|
|
8
|
+
every pair while exiting 0.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from clause.adapters import kalshi, limitless, polymarket, predictit # noqa: F401
|
|
12
|
+
|
|
13
|
+
__all__ = ["kalshi", "limitless", "polymarket", "predictit"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Venue adapter framework — one interface, twenty venues.
|
|
2
|
+
|
|
3
|
+
The market went from two venues that mattered to twenty-plus in eighteen
|
|
4
|
+
months, and it is still moving. An adapter has to be a day of work or the
|
|
5
|
+
coverage that is supposed to be our moat becomes our bottleneck.
|
|
6
|
+
|
|
7
|
+
What every adapter must declare, and why each one is enforced rather than
|
|
8
|
+
documented:
|
|
9
|
+
|
|
10
|
+
`storage_permitted` Limitless restricts data compilations, and any venue
|
|
11
|
+
whose terms are unread defaults to False.
|
|
12
|
+
A flag that lives in a comment gets ignored; this one
|
|
13
|
+
is checked before any write. Kalshi was the original
|
|
14
|
+
reason this exists and is no longer an example of it:
|
|
15
|
+
written authorisation on 2026-09-07 lifted its
|
|
16
|
+
Developer Agreement s3.1 restriction. The mechanism
|
|
17
|
+
outlived the case that motivated it, which is the
|
|
18
|
+
point of putting it in the type rather than a note.
|
|
19
|
+
`book_supported` not every venue exposes depth, and capacity silently
|
|
20
|
+
returning nothing is worse than an honest refusal.
|
|
21
|
+
`terms_field` which field carries resolution criteria. The whole
|
|
22
|
+
equivalence layer depends on finding it.
|
|
23
|
+
|
|
24
|
+
**When a venue's terms of service cannot be verified, the default is
|
|
25
|
+
restrictive.** That is the posture already taken with Limitless, and it is the
|
|
26
|
+
one that keeps us able to sell to venues rather than being blocked by them.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from abc import ABC, abstractmethod
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Iterator, Literal
|
|
34
|
+
|
|
35
|
+
from clause.execution.capacity import Book
|
|
36
|
+
|
|
37
|
+
VenueStatus = Literal["live", "beta", "planned", "blocked"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class VenueMarket:
|
|
42
|
+
"""A market as any venue describes it, normalised."""
|
|
43
|
+
venue: str
|
|
44
|
+
venue_market_id: str
|
|
45
|
+
title: str
|
|
46
|
+
body: str # resolution criteria as published
|
|
47
|
+
outcome_token_ids: tuple[str, ...] = ()
|
|
48
|
+
condition_id: str | None = None
|
|
49
|
+
slug: str | None = None
|
|
50
|
+
close_time: int | None = None
|
|
51
|
+
status: str = "open"
|
|
52
|
+
raw: dict = field(default_factory=dict, compare=False, repr=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class VenueCapabilities:
|
|
57
|
+
"""What an adapter can actually do. Absent capabilities are refused, not faked."""
|
|
58
|
+
markets: bool = True
|
|
59
|
+
terms: bool = True
|
|
60
|
+
book: bool = False
|
|
61
|
+
trades: bool = False
|
|
62
|
+
streaming: bool = False
|
|
63
|
+
order_construction: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class VenueAdapter(ABC):
|
|
67
|
+
"""Implement four methods and declare your permissions."""
|
|
68
|
+
|
|
69
|
+
venue: str = "unnamed"
|
|
70
|
+
display_name: str = "Unnamed"
|
|
71
|
+
status: VenueStatus = "planned"
|
|
72
|
+
|
|
73
|
+
# Hard gate, checked before persistence. Default deny.
|
|
74
|
+
storage_permitted: bool = False
|
|
75
|
+
tos_url: str | None = None
|
|
76
|
+
tos_verified_on: str | None = None
|
|
77
|
+
tos_note: str = "terms not verified — treated as restrictive"
|
|
78
|
+
|
|
79
|
+
capabilities: VenueCapabilities = VenueCapabilities()
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def list_markets(self, max_markets: int = 1000) -> Iterator[VenueMarket]:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def fetch_book(self, token_id: str) -> Book:
|
|
86
|
+
raise NotImplementedError(
|
|
87
|
+
f"{self.venue} does not expose order-book depth")
|
|
88
|
+
|
|
89
|
+
def close(self) -> None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# ---------- enforcement ----------
|
|
93
|
+
|
|
94
|
+
def assert_can_store(self) -> None:
|
|
95
|
+
"""Raise rather than write when the terms do not permit it.
|
|
96
|
+
|
|
97
|
+
Called at the persistence boundary. A permission expressed only as a
|
|
98
|
+
comment is a permission that gets ignored during a refactor.
|
|
99
|
+
"""
|
|
100
|
+
if not self.storage_permitted:
|
|
101
|
+
raise PermissionError(
|
|
102
|
+
f"{self.venue}: storage not permitted "
|
|
103
|
+
f"({self.tos_note}). Derived values only — raw content from "
|
|
104
|
+
f"this venue must not be persisted.")
|
|
105
|
+
|
|
106
|
+
def describe(self) -> dict:
|
|
107
|
+
return {
|
|
108
|
+
"venue": self.venue,
|
|
109
|
+
"display_name": self.display_name,
|
|
110
|
+
"status": self.status,
|
|
111
|
+
"storage_permitted": self.storage_permitted,
|
|
112
|
+
"tos": {"url": self.tos_url, "verified_on": self.tos_verified_on,
|
|
113
|
+
"note": self.tos_note},
|
|
114
|
+
"capabilities": {
|
|
115
|
+
"markets": self.capabilities.markets,
|
|
116
|
+
"terms": self.capabilities.terms,
|
|
117
|
+
"book": self.capabilities.book,
|
|
118
|
+
"trades": self.capabilities.trades,
|
|
119
|
+
"streaming": self.capabilities.streaming,
|
|
120
|
+
"order_construction": self.capabilities.order_construction,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Registry:
|
|
126
|
+
"""Every known venue, including the ones we deliberately do not ingest.
|
|
127
|
+
|
|
128
|
+
Blocked venues stay listed with their reason. A venue that silently
|
|
129
|
+
disappears from a registry looks like one nobody thought about; a venue
|
|
130
|
+
listed as blocked, with the clause that blocked it, is a decision.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
self._adapters: dict[str, type[VenueAdapter]] = {}
|
|
135
|
+
self._blocked: dict[str, str] = {}
|
|
136
|
+
|
|
137
|
+
def register(self, adapter_cls: type[VenueAdapter]) -> type[VenueAdapter]:
|
|
138
|
+
self._adapters[adapter_cls.venue] = adapter_cls
|
|
139
|
+
return adapter_cls
|
|
140
|
+
|
|
141
|
+
def block(self, venue: str, reason: str) -> None:
|
|
142
|
+
self._blocked[venue] = reason
|
|
143
|
+
|
|
144
|
+
def get(self, venue: str) -> type[VenueAdapter]:
|
|
145
|
+
if venue in self._blocked:
|
|
146
|
+
raise KeyError(f"{venue} is blocked: {self._blocked[venue]}")
|
|
147
|
+
if venue not in self._adapters:
|
|
148
|
+
raise KeyError(f"no adapter registered for {venue!r}")
|
|
149
|
+
return self._adapters[venue]
|
|
150
|
+
|
|
151
|
+
def storable(self) -> list[str]:
|
|
152
|
+
return [v for v, a in self._adapters.items() if a.storage_permitted]
|
|
153
|
+
|
|
154
|
+
def with_books(self) -> list[str]:
|
|
155
|
+
return [v for v, a in self._adapters.items() if a.capabilities.book]
|
|
156
|
+
|
|
157
|
+
def describe_all(self) -> dict:
|
|
158
|
+
return {
|
|
159
|
+
"registered": [a().describe() for a in self._adapters.values()],
|
|
160
|
+
"blocked": [{"venue": v, "reason": r}
|
|
161
|
+
for v, r in self._blocked.items()],
|
|
162
|
+
"counts": {
|
|
163
|
+
"registered": len(self._adapters),
|
|
164
|
+
"storable": len(self.storable()),
|
|
165
|
+
"with_books": len(self.with_books()),
|
|
166
|
+
"blocked": len(self._blocked),
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
REGISTRY = Registry()
|
|
172
|
+
|
|
173
|
+
# Recorded so the reasoning is not lost. Probed 2026-09-02.
|
|
174
|
+
REGISTRY.block(
|
|
175
|
+
"myriad",
|
|
176
|
+
"api.myriad.markets answers 'Service Healthy' but exposes no documented "
|
|
177
|
+
"market path; the Polkamarkets backend rejected all eleven pagination "
|
|
178
|
+
"conventions tried. Blocked pending real documentation.")
|
|
179
|
+
REGISTRY.block(
|
|
180
|
+
"coinbase",
|
|
181
|
+
"not a venue — Coinbase 'Predict' routes through Kalshi's matching engine.")
|
|
182
|
+
REGISTRY.block(
|
|
183
|
+
"kraken", "announced, not live as of 2026-09-02.")
|
|
184
|
+
REGISTRY.block(
|
|
185
|
+
"sporttrade",
|
|
186
|
+
"not trading: exited US sports betting 25 May 2026; CFTC DCM/DCO "
|
|
187
|
+
"application filed Feb 2026 and still pending as of 2026-09-09.")
|
|
188
|
+
|
|
189
|
+
# Verified 2026-09-09 by reading the clause, not a summary of it. This is
|
|
190
|
+
# the only venue found whose terms prohibit this product by description.
|
|
191
|
+
REGISTRY.block(
|
|
192
|
+
"sx_bet",
|
|
193
|
+
"terms forbid content being \"merged with other data\" or any "
|
|
194
|
+
"activity intended to \"collect, store, reorganise or manipulate\" it, "
|
|
195
|
+
"and separately prohibit scraping their odds. Cross-venue merging is "
|
|
196
|
+
"precisely this product, so there is no permitted reading. "
|
|
197
|
+
"help.sx.bet/en/articles/3613372-terms-and-conditions")
|
|
198
|
+
|
|
199
|
+
REGISTRY.block(
|
|
200
|
+
"metaculus",
|
|
201
|
+
"licence is limited, personal, non-exclusive and non-commercial, and "
|
|
202
|
+
"automated collection is barred except through their own API; they also "
|
|
203
|
+
"403 us outright. Verified 2026-09-09.")
|