delphi-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- delphi_kit/__init__.py +9 -0
- delphi_kit/abi/DelphiFactory.json +1 -0
- delphi_kit/abi/DpmGateway.json +1 -0
- delphi_kit/abi/DpmGatewayLegacy.json +1 -0
- delphi_kit/abi/LmsrGateway.json +1 -0
- delphi_kit/abi/LmsrMarket.json +1 -0
- delphi_kit/abi/__init__.py +0 -0
- delphi_kit/chain.py +135 -0
- delphi_kit/cli.py +191 -0
- delphi_kit/client.py +483 -0
- delphi_kit/limits.py +93 -0
- delphi_kit/mcp_server.py +180 -0
- delphi_kit/models.py +118 -0
- delphi_kit/networks.py +98 -0
- delphi_kit/py.typed +0 -0
- delphi_kit/search.py +40 -0
- delphi_kit/watch.py +209 -0
- delphi_kit-0.1.0.dist-info/METADATA +190 -0
- delphi_kit-0.1.0.dist-info/RECORD +21 -0
- delphi_kit-0.1.0.dist-info/WHEEL +4 -0
- delphi_kit-0.1.0.dist-info/entry_points.txt +4 -0
delphi_kit/limits.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Spending limits for agents. Every trade made through the MCP server (or any code that uses ``Guard``) is
|
|
2
|
+
checked against a per-trade cap, a rolling daily cap and an optional market allow-list, and recorded in a small
|
|
3
|
+
ledger file so the daily cap survives restarts."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LimitExceeded(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _env_float(name: str, default: float) -> float:
|
|
20
|
+
v = os.environ.get(name)
|
|
21
|
+
return float(v) if v not in (None, "") else default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Limits:
|
|
26
|
+
trading_enabled: bool = False
|
|
27
|
+
max_trade_usd: float = 5.0
|
|
28
|
+
max_daily_usd: float = 20.0
|
|
29
|
+
max_slippage: float = 0.03
|
|
30
|
+
allowed_markets: frozenset[str] | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_env(cls) -> "Limits":
|
|
34
|
+
allow = os.environ.get("DELPHI_ALLOWED_MARKETS")
|
|
35
|
+
return cls(
|
|
36
|
+
trading_enabled=os.environ.get("DELPHI_ALLOW_TRADING", "").lower() in ("1", "true", "yes"),
|
|
37
|
+
max_trade_usd=_env_float("DELPHI_MAX_TRADE_USD", 5.0),
|
|
38
|
+
max_daily_usd=_env_float("DELPHI_MAX_DAILY_USD", 20.0),
|
|
39
|
+
max_slippage=_env_float("DELPHI_MAX_SLIPPAGE", 0.03),
|
|
40
|
+
allowed_markets=frozenset(a.strip().lower() for a in allow.split(",") if a.strip()) if allow else None,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Guard:
|
|
45
|
+
def __init__(self, limits: Limits, ledger: Path | None = None):
|
|
46
|
+
self.limits = limits
|
|
47
|
+
base = Path(os.environ.get("DELPHI_KIT_CACHE") or Path.home() / ".cache" / "delphi-kit")
|
|
48
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
self.ledger = ledger or base / "spend.json"
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
|
|
52
|
+
def _today(self) -> str:
|
|
53
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
54
|
+
|
|
55
|
+
def _read(self) -> dict:
|
|
56
|
+
try:
|
|
57
|
+
return json.loads(self.ledger.read_text())
|
|
58
|
+
except (FileNotFoundError, ValueError):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
def spent_today(self) -> float:
|
|
62
|
+
return float(self._read().get(self._today(), 0.0))
|
|
63
|
+
|
|
64
|
+
def check(self, market: str, usd: float) -> None:
|
|
65
|
+
lim = self.limits
|
|
66
|
+
if not lim.trading_enabled:
|
|
67
|
+
raise LimitExceeded("trading is off. Set DELPHI_ALLOW_TRADING=true to let this agent trade.")
|
|
68
|
+
if lim.allowed_markets is not None and market.lower() not in lim.allowed_markets:
|
|
69
|
+
raise LimitExceeded("this market is not in DELPHI_ALLOWED_MARKETS")
|
|
70
|
+
if usd > lim.max_trade_usd + 1e-9:
|
|
71
|
+
raise LimitExceeded(f"${usd:.2f} is over the per-trade limit of ${lim.max_trade_usd:.2f} (DELPHI_MAX_TRADE_USD)")
|
|
72
|
+
left = lim.max_daily_usd - self.spent_today()
|
|
73
|
+
if usd > left + 1e-9:
|
|
74
|
+
raise LimitExceeded(f"${usd:.2f} is over today's remaining budget of ${max(0.0, left):.2f} (DELPHI_MAX_DAILY_USD)")
|
|
75
|
+
|
|
76
|
+
def record(self, usd: float) -> None:
|
|
77
|
+
with self._lock:
|
|
78
|
+
data = self._read()
|
|
79
|
+
day = self._today()
|
|
80
|
+
data[day] = round(float(data.get(day, 0.0)) + usd, 6)
|
|
81
|
+
data = {k: v for k, v in data.items() if k >= sorted(data)[-30:][0]} # keep 30 days
|
|
82
|
+
self.ledger.write_text(json.dumps(data, indent=1))
|
|
83
|
+
|
|
84
|
+
def status(self) -> dict:
|
|
85
|
+
lim = self.limits
|
|
86
|
+
return {
|
|
87
|
+
"trading_enabled": lim.trading_enabled,
|
|
88
|
+
"max_trade_usd": lim.max_trade_usd,
|
|
89
|
+
"max_daily_usd": lim.max_daily_usd,
|
|
90
|
+
"spent_today_usd": round(self.spent_today(), 4),
|
|
91
|
+
"max_slippage": lim.max_slippage,
|
|
92
|
+
"allowed_markets": sorted(lim.allowed_markets) if lim.allowed_markets is not None else "all",
|
|
93
|
+
}
|
delphi_kit/mcp_server.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""An MCP server that lets any AI agent (Claude, Cursor, Hermes, your own) read and trade Delphi markets.
|
|
2
|
+
|
|
3
|
+
Reading needs nothing. Trading is off until you set ``DELPHI_ALLOW_TRADING=true`` and a wallet key, and every
|
|
4
|
+
trade is held to a per-trade cap, a daily cap and a slippage cap (see ``limits.py``).
|
|
5
|
+
|
|
6
|
+
uvx --from "delphi-kit[mcp]" delphi-mcp # stdio, for Claude Desktop / Claude Code / Cursor
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from mcp.server.mcpserver import MCPServer
|
|
15
|
+
from mcp.types import ToolAnnotations
|
|
16
|
+
|
|
17
|
+
from .client import Delphi, DelphiError
|
|
18
|
+
from .limits import Guard, LimitExceeded, Limits
|
|
19
|
+
|
|
20
|
+
INSTRUCTIONS = """Delphi is a prediction market on the Gensyn chain, settled by an AI judge that reads each
|
|
21
|
+
market's rules and named sources. Prices are probabilities (0-1). Before buying: read the market's rules and
|
|
22
|
+
sources with get_market, quote with quote_buy, and check probability_after: thin markets move a lot on small
|
|
23
|
+
trades. Trades are held to spending limits shown by wallet_status; never try to work around them."""
|
|
24
|
+
|
|
25
|
+
READ = ToolAnnotations(read_only_hint=True, open_world_hint=True)
|
|
26
|
+
WRITE = ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False, open_world_hint=True)
|
|
27
|
+
|
|
28
|
+
server = MCPServer(name="delphi", instructions=INSTRUCTIONS)
|
|
29
|
+
_client: Optional[Delphi] = None
|
|
30
|
+
_guard: Optional[Guard] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def client() -> Delphi:
|
|
34
|
+
global _client
|
|
35
|
+
if _client is None:
|
|
36
|
+
_client = Delphi(os.environ.get("DELPHI_NETWORK", "mainnet"))
|
|
37
|
+
return _client
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def guard() -> Guard:
|
|
41
|
+
global _guard
|
|
42
|
+
if _guard is None:
|
|
43
|
+
_guard = Guard(Limits.from_env())
|
|
44
|
+
return _guard
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _brief(m) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"address": m.address, "question": m.question, "status": m.status,
|
|
50
|
+
"outcomes": [{"label": o, "probability": round(p, 4)} for o, p in zip(m.outcomes, m.probabilities)],
|
|
51
|
+
"trading_closes_at": m.trading_closes_at.isoformat() if m.trading_closes_at else None,
|
|
52
|
+
"pool_usd": round(m.pool_usd or 0, 2), "type": m.deployment,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _fail(e: Exception) -> dict:
|
|
57
|
+
return {"error": str(e)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@server.tool(annotations=READ)
|
|
61
|
+
def list_markets(status: str = "open", query: str = "", limit: int = 20) -> Any:
|
|
62
|
+
"""List Delphi markets with live probabilities. status: open | awaiting_settlement | settled | expired | failed | all.
|
|
63
|
+
query: optional keywords to search questions, outcomes and rules. Open markets come soonest-closing first."""
|
|
64
|
+
try:
|
|
65
|
+
st = None if status == "all" else status
|
|
66
|
+
ms = client().search(query, status=st, limit=limit) if query.strip() else client().markets(status=st, limit=limit)
|
|
67
|
+
return [_brief(m) for m in ms]
|
|
68
|
+
except (DelphiError, ValueError) as e:
|
|
69
|
+
return _fail(e)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@server.tool(annotations=READ)
|
|
73
|
+
def get_market(market: str) -> Any:
|
|
74
|
+
"""Everything about one market: question, outcomes and probabilities, the rules the AI judge will apply,
|
|
75
|
+
its sources, deadlines, pool size, fee, and whether the rules file matches its on-chain hash."""
|
|
76
|
+
try:
|
|
77
|
+
m = client().market(market)
|
|
78
|
+
d = m.to_dict()
|
|
79
|
+
d.pop("raw_metadata", None)
|
|
80
|
+
return d
|
|
81
|
+
except (DelphiError, ValueError) as e:
|
|
82
|
+
return _fail(e)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@server.tool(annotations=READ)
|
|
86
|
+
def quote_buy(market: str, outcome: str, usd: float) -> Any:
|
|
87
|
+
"""What spending `usd` on `outcome` (label like "Yes" or an index) buys right now: shares, average price,
|
|
88
|
+
and the probability after the trade. Fees included; nothing is sent."""
|
|
89
|
+
try:
|
|
90
|
+
return client().quote_buy(market, outcome, usd=usd).to_dict()
|
|
91
|
+
except (DelphiError, ValueError) as e:
|
|
92
|
+
return _fail(e)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@server.tool(annotations=READ)
|
|
96
|
+
def quote_sell(market: str, outcome: str, shares: float) -> Any:
|
|
97
|
+
"""What selling `shares` of `outcome` back to the market returns right now. Nothing is sent."""
|
|
98
|
+
try:
|
|
99
|
+
return client().quote_sell(market, outcome, shares).to_dict()
|
|
100
|
+
except (DelphiError, ValueError) as e:
|
|
101
|
+
return _fail(e)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@server.tool(annotations=READ)
|
|
105
|
+
def get_positions(wallet: str = "") -> Any:
|
|
106
|
+
"""Shares a wallet holds across every Delphi market, their current value, and what can be collected
|
|
107
|
+
(redeem after settlement, liquidate after a failed or expired market). Defaults to the agent's own wallet."""
|
|
108
|
+
try:
|
|
109
|
+
return [p.to_dict() for p in client().positions(wallet or None)]
|
|
110
|
+
except (DelphiError, ValueError) as e:
|
|
111
|
+
return _fail(e)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@server.tool(annotations=READ)
|
|
115
|
+
def wallet_status() -> Any:
|
|
116
|
+
"""The agent's wallet address, USDC and gas balance, and its spending limits and spend so far today."""
|
|
117
|
+
c = client()
|
|
118
|
+
out: dict[str, Any] = {"network": c.network.name, "limits": guard().status()}
|
|
119
|
+
if c.address:
|
|
120
|
+
try:
|
|
121
|
+
out.update(c.balance())
|
|
122
|
+
except Exception as e: # noqa: BLE001
|
|
123
|
+
out["balance_error"] = str(e)
|
|
124
|
+
else:
|
|
125
|
+
out["wallet"] = None
|
|
126
|
+
out["note"] = "Read-only: no DELPHI_PRIVATE_KEY set."
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@server.tool(annotations=WRITE)
|
|
131
|
+
def buy(market: str, outcome: str, usd: float) -> Any:
|
|
132
|
+
"""Buy shares of `outcome` with up to `usd` of USDC. Held to the per-trade, daily and slippage limits.
|
|
133
|
+
Quote first and read the rules; this sends a real transaction."""
|
|
134
|
+
g = guard()
|
|
135
|
+
try:
|
|
136
|
+
g.check(market, usd)
|
|
137
|
+
t = client().buy(market, outcome, usd=usd, max_slippage=g.limits.max_slippage)
|
|
138
|
+
g.record(t.usd)
|
|
139
|
+
return t.to_dict()
|
|
140
|
+
except (LimitExceeded, DelphiError, ValueError) as e:
|
|
141
|
+
return _fail(e)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@server.tool(annotations=WRITE)
|
|
145
|
+
def sell(market: str, outcome: str, shares: float = 0) -> Any:
|
|
146
|
+
"""Sell `shares` of `outcome` back to the market (0 = everything held). Sends a real transaction."""
|
|
147
|
+
g = guard()
|
|
148
|
+
try:
|
|
149
|
+
if not g.limits.trading_enabled:
|
|
150
|
+
raise LimitExceeded("trading is off. Set DELPHI_ALLOW_TRADING=true to let this agent trade.")
|
|
151
|
+
return client().sell(market, outcome, shares or None, max_slippage=g.limits.max_slippage).to_dict()
|
|
152
|
+
except (LimitExceeded, DelphiError, ValueError) as e:
|
|
153
|
+
return _fail(e)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@server.tool(annotations=WRITE)
|
|
157
|
+
def collect(market: str) -> Any:
|
|
158
|
+
"""Collect what a closed market owes the agent: redeems winnings from a settled market, or liquidates
|
|
159
|
+
shares in an expired or failed market at their final prices."""
|
|
160
|
+
g = guard()
|
|
161
|
+
try:
|
|
162
|
+
if not g.limits.trading_enabled:
|
|
163
|
+
raise LimitExceeded("trading is off. Set DELPHI_ALLOW_TRADING=true to let this agent trade.")
|
|
164
|
+
c = client()
|
|
165
|
+
m = c.market(market, with_rules=False)
|
|
166
|
+
if m.status == "settled":
|
|
167
|
+
return c.redeem(market)
|
|
168
|
+
if m.status in ("expired", "failed"):
|
|
169
|
+
return c.liquidate(market)
|
|
170
|
+
return _fail(DelphiError(f"market is {m.status}; nothing to collect yet"))
|
|
171
|
+
except (LimitExceeded, DelphiError, ValueError) as e:
|
|
172
|
+
return _fail(e)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main() -> None:
|
|
176
|
+
server.run("stdio")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
main()
|
delphi_kit/models.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Plain data objects returned by the client. Money is in USD (the collateral token), shares are decimal numbers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
STATUSES = ("open", "awaiting_settlement", "settled", "expired", "failed")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ts(seconds: int | None) -> Optional[datetime]:
|
|
13
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc) if seconds else None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _jsonable(v: Any) -> Any:
|
|
17
|
+
if isinstance(v, datetime):
|
|
18
|
+
return v.isoformat()
|
|
19
|
+
if isinstance(v, dict):
|
|
20
|
+
return {k: _jsonable(x) for k, x in v.items()}
|
|
21
|
+
if isinstance(v, (list, tuple)):
|
|
22
|
+
return [_jsonable(x) for x in v]
|
|
23
|
+
return v
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Dict:
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
return _jsonable(asdict(self)) # type: ignore[arg-type]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Market(_Dict):
|
|
33
|
+
address: str
|
|
34
|
+
deployment: str # "lmsr" | "dpm" | "dpm-legacy"
|
|
35
|
+
mechanism: str # "lmsr" | "dpm"
|
|
36
|
+
status: str # one of STATUSES
|
|
37
|
+
question: Optional[str]
|
|
38
|
+
outcomes: list[str]
|
|
39
|
+
probabilities: list[float] # implied probability per outcome, sums to ~1
|
|
40
|
+
prices: list[float] # USD cost of one more share, per outcome (spot)
|
|
41
|
+
trading_closes_at: Optional[datetime]
|
|
42
|
+
settlement_deadline: Optional[datetime]
|
|
43
|
+
earliest_resolve_at: Optional[datetime] = None
|
|
44
|
+
winning_outcome: Optional[int] = None
|
|
45
|
+
pool_usd: Optional[float] = None
|
|
46
|
+
trading_fee: Optional[float] = None
|
|
47
|
+
rules: Optional[str] = None # what the AI judge is told to check
|
|
48
|
+
sources: list[str] = field(default_factory=list)
|
|
49
|
+
judge_model: Optional[str] = None
|
|
50
|
+
metadata_uri: Optional[str] = None
|
|
51
|
+
metadata_verified: Optional[bool] = None # True when the rules file matches its on-chain hash
|
|
52
|
+
url: Optional[str] = None
|
|
53
|
+
gateway: Optional[str] = None
|
|
54
|
+
raw_metadata: Optional[dict] = field(default=None, repr=False)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_open(self) -> bool:
|
|
58
|
+
return self.status == "open"
|
|
59
|
+
|
|
60
|
+
def outcome_index(self, outcome: int | str) -> int:
|
|
61
|
+
"""Accept an index or a label ("Yes", "no", ...)."""
|
|
62
|
+
if isinstance(outcome, int) or (isinstance(outcome, str) and outcome.isdigit()):
|
|
63
|
+
i = int(outcome)
|
|
64
|
+
if not 0 <= i < len(self.outcomes or self.probabilities):
|
|
65
|
+
raise ValueError(f"outcome {i} out of range for {self.address}")
|
|
66
|
+
return i
|
|
67
|
+
labels = [o.lower().strip() for o in self.outcomes]
|
|
68
|
+
try:
|
|
69
|
+
return labels.index(str(outcome).lower().strip())
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise ValueError(f"{outcome!r} is not an outcome of this market; outcomes are {self.outcomes}") from None
|
|
72
|
+
|
|
73
|
+
def summary(self) -> str:
|
|
74
|
+
odds = ", ".join(f"{o} {p:.0%}" for o, p in zip(self.outcomes, self.probabilities))
|
|
75
|
+
close = self.trading_closes_at.strftime("%Y-%m-%d %H:%M UTC") if self.trading_closes_at else "?"
|
|
76
|
+
return f"{self.question} [{odds}] {self.status}, trading closes {close}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Quote(_Dict):
|
|
81
|
+
market: str
|
|
82
|
+
outcome: int
|
|
83
|
+
outcome_label: Optional[str]
|
|
84
|
+
side: str # "buy" | "sell"
|
|
85
|
+
shares: float
|
|
86
|
+
usd: float # cost for a buy, proceeds for a sell (fees included)
|
|
87
|
+
average_price: Optional[float]
|
|
88
|
+
probability_before: Optional[float]
|
|
89
|
+
probability_after: Optional[float] = None
|
|
90
|
+
shares_raw: int = 0
|
|
91
|
+
usd_raw: int = 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Trade(_Dict):
|
|
96
|
+
market: str
|
|
97
|
+
outcome: int
|
|
98
|
+
outcome_label: Optional[str]
|
|
99
|
+
side: str
|
|
100
|
+
shares: float
|
|
101
|
+
usd: float
|
|
102
|
+
tx_hash: str
|
|
103
|
+
explorer_url: str
|
|
104
|
+
approval_tx: Optional[str] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class Position(_Dict):
|
|
109
|
+
market: str
|
|
110
|
+
question: Optional[str]
|
|
111
|
+
outcome: int
|
|
112
|
+
outcome_label: Optional[str]
|
|
113
|
+
shares: float
|
|
114
|
+
probability: Optional[float]
|
|
115
|
+
value_usd: Optional[float] # shares x current probability (open markets) or payout (settled)
|
|
116
|
+
status: str
|
|
117
|
+
action: Optional[str] = None # "redeem" | "liquidate" | None
|
|
118
|
+
claimable_usd: Optional[float] = None
|
delphi_kit/networks.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Where Delphi lives on each network.
|
|
2
|
+
|
|
3
|
+
Delphi has shipped three generations of contracts. Each generation has its own factory (which creates markets)
|
|
4
|
+
and gateway (which every trade, quote and settlement goes through). A market belongs to exactly one generation
|
|
5
|
+
for life, so every call has to be sent to the right gateway. The official TypeScript SDK 2.1.0 knows the two
|
|
6
|
+
pari-mutuel generations; the LMSR generation, which carries live trading on mainnet since September 2026, is
|
|
7
|
+
listed here too.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Deployment:
|
|
17
|
+
name: str # short label, e.g. "lmsr"
|
|
18
|
+
kind: str # "lmsr" | "dpm" | "dpm_legacy" (selects the ABI)
|
|
19
|
+
factory: str
|
|
20
|
+
gateway: str
|
|
21
|
+
note: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Network:
|
|
26
|
+
name: str
|
|
27
|
+
chain_id: int
|
|
28
|
+
rpc_url: str
|
|
29
|
+
token: str # USDC (or the competition token)
|
|
30
|
+
token_decimals: int
|
|
31
|
+
multicall3: str
|
|
32
|
+
explorer_url: str
|
|
33
|
+
app_url: str
|
|
34
|
+
deployments: tuple[Deployment, ...] = field(default_factory=tuple)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
38
|
+
|
|
39
|
+
NETWORKS: dict[str, Network] = {
|
|
40
|
+
"mainnet": Network(
|
|
41
|
+
name="mainnet",
|
|
42
|
+
chain_id=685689,
|
|
43
|
+
rpc_url="https://gensyn-mainnet.g.alchemy.com/public",
|
|
44
|
+
token="0x5b32c997211621d55a89Cc5abAF1cC21F3A6ddF5",
|
|
45
|
+
token_decimals=6,
|
|
46
|
+
multicall3=MULTICALL3,
|
|
47
|
+
explorer_url="https://gensyn-mainnet.explorer.alchemy.com",
|
|
48
|
+
app_url="https://delphi.fyi",
|
|
49
|
+
deployments=(
|
|
50
|
+
Deployment("lmsr", "lmsr", "0x1819a2aeEb96DA9BA1C5262A5c82B6F9c347f99c",
|
|
51
|
+
"0x7fd914D05D1FE4Fa3EC84E2Cd1D81be6CBe03747",
|
|
52
|
+
"LMSR, AI-settled. Live trading since Sep 2026. Not in SDK 2.1.0 defaults."),
|
|
53
|
+
Deployment("dpm", "dpm", "0x9C73417f79a1361c6aF9Bd828343badEE1b84936",
|
|
54
|
+
"0x982a67aE92D8de361957249fB2BB4a62BCc6A8d5",
|
|
55
|
+
"Pari-mutuel, AI-settled."),
|
|
56
|
+
Deployment("dpm-legacy", "dpm_legacy", "0x4596d847eA56DCf9A37944c13793Af802Fc5D1eC",
|
|
57
|
+
"0x4e4e85c52E0F414cc67eE88d0C649Ec81698d700",
|
|
58
|
+
"Pari-mutuel, creator-settled. First generation."),
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
"testnet": Network(
|
|
62
|
+
name="testnet",
|
|
63
|
+
chain_id=685685,
|
|
64
|
+
rpc_url="https://gensyn-testnet.g.alchemy.com/public",
|
|
65
|
+
token="0x0724D6079b986F8e44bDafB8a09B60C0bd6A45a1",
|
|
66
|
+
token_decimals=6,
|
|
67
|
+
multicall3=MULTICALL3,
|
|
68
|
+
explorer_url="https://gensyn-testnet.explorer.alchemy.com",
|
|
69
|
+
app_url="https://testnet.delphi.fyi",
|
|
70
|
+
deployments=(
|
|
71
|
+
Deployment("dpm", "dpm", "0x97d2b3F0614C8189343A38094629FCE2910b727A",
|
|
72
|
+
"0x22ea355D7218Dc86b4c83732cBbd01f7Ff2332b3"),
|
|
73
|
+
Deployment("dpm-legacy", "dpm_legacy", "0xd03CEC55802f0D44D844384E1144B25717315E5A",
|
|
74
|
+
"0x7b8FDBD187B0Be5e30e48B1995df574A62667147"),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
"competition-testnet": Network(
|
|
78
|
+
name="competition-testnet",
|
|
79
|
+
chain_id=685685,
|
|
80
|
+
rpc_url="https://gensyn-testnet.g.alchemy.com/public",
|
|
81
|
+
token="0x8A2d75753362Eb5D5669a2c22cbf394b26a0571F",
|
|
82
|
+
token_decimals=6,
|
|
83
|
+
multicall3=MULTICALL3,
|
|
84
|
+
explorer_url="https://gensyn-testnet.explorer.alchemy.com",
|
|
85
|
+
app_url="https://agent-competition.gensyn.ai",
|
|
86
|
+
deployments=(
|
|
87
|
+
Deployment("lmsr", "lmsr", "0xEa9D0a78d0209916e88e363B8FDa3e23206Ff49b",
|
|
88
|
+
"0x097599c9D966fF496284b892A8F13BF885b258ef", "Agent competition."),
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_network(name: str) -> Network:
|
|
95
|
+
try:
|
|
96
|
+
return NETWORKS[name]
|
|
97
|
+
except KeyError:
|
|
98
|
+
raise ValueError(f"unknown network {name!r}; choose one of {', '.join(NETWORKS)}") from None
|
delphi_kit/py.typed
ADDED
|
File without changes
|
delphi_kit/search.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Keyword search over markets. Small and dependency-free: word matches in the question count most, then
|
|
2
|
+
outcomes, then the rules; prefixes count ("elect" finds "election"); ties go to the market that closes first."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .models import Market
|
|
10
|
+
|
|
11
|
+
_WORD = re.compile(r"[a-z0-9$%.]+")
|
|
12
|
+
_STOP = {"the", "a", "an", "of", "in", "on", "by", "to", "will", "be", "is", "for", "and", "or", "at", "before", "after"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _words(text: str | None) -> list[str]:
|
|
16
|
+
return [w.strip(".") for w in _WORD.findall((text or "").lower()) if w.strip(".") and w not in _STOP]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def score(m: Market, terms: list[str]) -> float:
|
|
20
|
+
fields = ((_words(m.question), 3.0), (_words(" ".join(m.outcomes)), 2.0), (_words(m.rules), 1.0))
|
|
21
|
+
total = 0.0
|
|
22
|
+
for t in terms:
|
|
23
|
+
best = 0.0
|
|
24
|
+
for words, weight in fields:
|
|
25
|
+
if t in words:
|
|
26
|
+
best = max(best, weight)
|
|
27
|
+
elif any(w.startswith(t) for w in words) and len(t) >= 3:
|
|
28
|
+
best = max(best, weight * 0.6)
|
|
29
|
+
total += best
|
|
30
|
+
return total
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rank(markets: list[Market], query: str) -> list[Market]:
|
|
34
|
+
terms = _words(query)
|
|
35
|
+
if not terms:
|
|
36
|
+
return markets
|
|
37
|
+
scored = [(score(m, terms), m) for m in markets]
|
|
38
|
+
scored = [(s, m) for s, m in scored if s > 0]
|
|
39
|
+
scored.sort(key=lambda x: (-x[0], x[1].trading_closes_at.timestamp() if x[1].trading_closes_at else math.inf))
|
|
40
|
+
return [m for _, m in scored]
|