delphi-kit 0.1.1__tar.gz → 0.2.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.
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/PKG-INFO +36 -2
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/README.md +35 -1
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/pyproject.toml +1 -1
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/pyproject.toml.orig +1 -1
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/__init__.py +1 -1
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/cli.py +14 -3
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/client.py +46 -4
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/mcp_server.py +35 -6
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/models.py +1 -0
- delphi_kit-0.2.0/src/delphi_kit/recorder.py +204 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/DelphiFactory.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/DpmGateway.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/DpmGatewayLegacy.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/LmsrGateway.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/LmsrGatewayV1.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/LmsrMarket.json +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/abi/__init__.py +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/chain.py +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/limits.py +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/networks.py +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/py.typed +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/search.py +0 -0
- {delphi_kit-0.1.1 → delphi_kit-0.2.0}/src/delphi_kit/watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: delphi-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Read and trade Delphi prediction markets from Python and AI agents, with no API key: every market type, quotes, trades, positions, search, alerts and an MCP server.
|
|
5
5
|
Keywords: delphi,gensyn,prediction-markets,mcp,agents,web3
|
|
6
6
|
License-Expression: MIT
|
|
@@ -120,7 +120,7 @@ Claude Desktop, Cursor, or anything else that speaks MCP over stdio:
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
The read tools are `list_markets`, `get_market`, `quote_buy`, `quote_sell`, `get_positions` and `
|
|
123
|
+
The read tools are `list_markets`, `get_market`, `quote_buy`, `quote_sell`, `get_positions`, `wallet_status` and `my_record`.
|
|
124
124
|
The write tools are `buy`, `sell` and `collect`.
|
|
125
125
|
|
|
126
126
|
Without a key or `DELPHI_ALLOW_TRADING=true` the server is read-only, and the write tools say so instead of
|
|
@@ -135,6 +135,37 @@ sending anything. With trading on, every buy is checked first:
|
|
|
135
135
|
|
|
136
136
|
Use a separate wallet for the agent and only put in what you're willing to lose.
|
|
137
137
|
|
|
138
|
+
## Flight recorder: a record nobody can edit
|
|
139
|
+
|
|
140
|
+
Turn on `record=True` and every buy must first state what the agent believes. That belief is signed by the
|
|
141
|
+
trading wallet, sealed until trading closes, and anchored on Gensyn mainnet by [Agon](https://agon.nerom.site).
|
|
142
|
+
Nobody can see it before then or change it afterwards. When the market settles, the agent's record writes itself:
|
|
143
|
+
|
|
144
|
+
- **Calibration:** were its probabilities better than the market's price at the time?
|
|
145
|
+
- **Results:** what the same wallet actually bought and sold, read from the chain.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
d = Delphi(private_key=key, record=True, agent_name="my-agent")
|
|
149
|
+
t = d.buy(m, "Yes", usd=2, belief={"Yes": 0.71}, reasoning="Polls moved after the debate.")
|
|
150
|
+
t.record # {'entry': 200, 'belief': 0.71, 'market_probability': 0.50, 'edge': 0.21,
|
|
151
|
+
# 'sealed_until': '2027-12-31T23:00:00Z', 'record_url': 'https://agon.nerom.site/#/agent/0x…', ...}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
delphi buy 0x4F5D… Yes --usd 2 --record --belief 0.71 --reason "Polls moved after the debate."
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- **No entry, no trade.** A buy without a belief is refused, and so is one whose entry can't be logged.
|
|
159
|
+
`record="try"` trades anyway and says why the entry is missing.
|
|
160
|
+
- **When it's logged.** The entry is sent after the trade has been simulated, so failed buys never leave entries.
|
|
161
|
+
- **What it covers.** Mainnet markets whose trading window was at least 12 hours long; shorter ones are refused.
|
|
162
|
+
- **Which server.** Entries go to https://agon.nerom.site. Agon is open source; point `recorder_url` or
|
|
163
|
+
`DELPHI_RECORDER_URL` at your own instance if you run one.
|
|
164
|
+
- **In the MCP server.** Set `DELPHI_RECORD=true` (plus `DELPHI_AGENT_NAME`) and the `buy` tool requires the
|
|
165
|
+
agent's `probability` and `reasoning`. `my_record` shows what it has logged and, after settlement, how it scored.
|
|
166
|
+
- **Proof for the model, too.** An entry can carry a receipt from Gensyn's REE (`receipt=`)
|
|
167
|
+
proving which open model produced the forecast. Agon re-runs receipts on a second machine.
|
|
168
|
+
|
|
138
169
|
## Alerts
|
|
139
170
|
|
|
140
171
|
Delphi has no notifications or webhooks, so `delphi watch` polls the chain and tells you about:
|
|
@@ -171,6 +202,7 @@ notes are in [docs/contracts.md](docs/contracts.md).
|
|
|
171
202
|
| Default approval | unlimited | exact amount |
|
|
172
203
|
| Agent spending limits | no | yes |
|
|
173
204
|
| Alerts | no | yes |
|
|
205
|
+
| Sealed, scored record of what an agent believed | no | yes (`record=True`) |
|
|
174
206
|
|
|
175
207
|
## Tests
|
|
176
208
|
|
|
@@ -184,6 +216,8 @@ The fork tests run a private copy of Gensyn mainnet and put a throwaway wallet t
|
|
|
184
216
|
- **LMSR:** buy with a $3 budget, sell half, jump the clock past the deadline, settle as Delphi's oracle relayer,
|
|
185
217
|
redeem the winnings.
|
|
186
218
|
- **DPM:** buy, let the market expire unsettled, liquidate.
|
|
219
|
+
- **Flight recorder** (with `DELPHI_TEST_RECORDER_URL` pointing at a local Agon): a buy without a belief is
|
|
220
|
+
refused, then a belief is signed, accepted by Agon and sealed before the buy is sent.
|
|
187
221
|
|
|
188
222
|
## License
|
|
189
223
|
|
|
@@ -106,7 +106,7 @@ Claude Desktop, Cursor, or anything else that speaks MCP over stdio:
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
The read tools are `list_markets`, `get_market`, `quote_buy`, `quote_sell`, `get_positions` and `
|
|
109
|
+
The read tools are `list_markets`, `get_market`, `quote_buy`, `quote_sell`, `get_positions`, `wallet_status` and `my_record`.
|
|
110
110
|
The write tools are `buy`, `sell` and `collect`.
|
|
111
111
|
|
|
112
112
|
Without a key or `DELPHI_ALLOW_TRADING=true` the server is read-only, and the write tools say so instead of
|
|
@@ -121,6 +121,37 @@ sending anything. With trading on, every buy is checked first:
|
|
|
121
121
|
|
|
122
122
|
Use a separate wallet for the agent and only put in what you're willing to lose.
|
|
123
123
|
|
|
124
|
+
## Flight recorder: a record nobody can edit
|
|
125
|
+
|
|
126
|
+
Turn on `record=True` and every buy must first state what the agent believes. That belief is signed by the
|
|
127
|
+
trading wallet, sealed until trading closes, and anchored on Gensyn mainnet by [Agon](https://agon.nerom.site).
|
|
128
|
+
Nobody can see it before then or change it afterwards. When the market settles, the agent's record writes itself:
|
|
129
|
+
|
|
130
|
+
- **Calibration:** were its probabilities better than the market's price at the time?
|
|
131
|
+
- **Results:** what the same wallet actually bought and sold, read from the chain.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
d = Delphi(private_key=key, record=True, agent_name="my-agent")
|
|
135
|
+
t = d.buy(m, "Yes", usd=2, belief={"Yes": 0.71}, reasoning="Polls moved after the debate.")
|
|
136
|
+
t.record # {'entry': 200, 'belief': 0.71, 'market_probability': 0.50, 'edge': 0.21,
|
|
137
|
+
# 'sealed_until': '2027-12-31T23:00:00Z', 'record_url': 'https://agon.nerom.site/#/agent/0x…', ...}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
delphi buy 0x4F5D… Yes --usd 2 --record --belief 0.71 --reason "Polls moved after the debate."
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **No entry, no trade.** A buy without a belief is refused, and so is one whose entry can't be logged.
|
|
145
|
+
`record="try"` trades anyway and says why the entry is missing.
|
|
146
|
+
- **When it's logged.** The entry is sent after the trade has been simulated, so failed buys never leave entries.
|
|
147
|
+
- **What it covers.** Mainnet markets whose trading window was at least 12 hours long; shorter ones are refused.
|
|
148
|
+
- **Which server.** Entries go to https://agon.nerom.site. Agon is open source; point `recorder_url` or
|
|
149
|
+
`DELPHI_RECORDER_URL` at your own instance if you run one.
|
|
150
|
+
- **In the MCP server.** Set `DELPHI_RECORD=true` (plus `DELPHI_AGENT_NAME`) and the `buy` tool requires the
|
|
151
|
+
agent's `probability` and `reasoning`. `my_record` shows what it has logged and, after settlement, how it scored.
|
|
152
|
+
- **Proof for the model, too.** An entry can carry a receipt from Gensyn's REE (`receipt=`)
|
|
153
|
+
proving which open model produced the forecast. Agon re-runs receipts on a second machine.
|
|
154
|
+
|
|
124
155
|
## Alerts
|
|
125
156
|
|
|
126
157
|
Delphi has no notifications or webhooks, so `delphi watch` polls the chain and tells you about:
|
|
@@ -157,6 +188,7 @@ notes are in [docs/contracts.md](docs/contracts.md).
|
|
|
157
188
|
| Default approval | unlimited | exact amount |
|
|
158
189
|
| Agent spending limits | no | yes |
|
|
159
190
|
| Alerts | no | yes |
|
|
191
|
+
| Sealed, scored record of what an agent believed | no | yes (`record=True`) |
|
|
160
192
|
|
|
161
193
|
## Tests
|
|
162
194
|
|
|
@@ -170,6 +202,8 @@ The fork tests run a private copy of Gensyn mainnet and put a throwaway wallet t
|
|
|
170
202
|
- **LMSR:** buy with a $3 budget, sell half, jump the clock past the deadline, settle as Delphi's oracle relayer,
|
|
171
203
|
redeem the winnings.
|
|
172
204
|
- **DPM:** buy, let the market expire unsettled, liquidate.
|
|
205
|
+
- **Flight recorder** (with `DELPHI_TEST_RECORDER_URL` pointing at a local Agon): a buy without a belief is
|
|
206
|
+
refused, then a belief is signed, accepted by Agon and sealed before the buy is sent.
|
|
173
207
|
|
|
174
208
|
## License
|
|
175
209
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "delphi-kit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Read and trade Delphi prediction markets from Python and AI agents, with no API key: every market type, quotes, trades, positions, search, alerts and an MCP server."
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "delphi-kit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Read and trade Delphi prediction markets from Python and AI agents, with no API key: every market type, quotes, trades, positions, search, alerts and an MCP server."
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
readme = "README.md"
|
|
@@ -53,6 +53,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
53
53
|
s.add_argument("--usd", type=float, required=True)
|
|
54
54
|
s.add_argument("--max-slippage", type=float, default=0.02)
|
|
55
55
|
s.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
|
|
56
|
+
s.add_argument("--record", action="store_true", help="flight recorder: seal --belief on Agon before buying")
|
|
57
|
+
s.add_argument("--belief", type=float, help="your probability (0-1) that this outcome wins")
|
|
58
|
+
s.add_argument("--reason", default=None, help="why, sealed with the belief")
|
|
56
59
|
|
|
57
60
|
s = sub.add_parser("sell", help="sell shares (needs DELPHI_PRIVATE_KEY)")
|
|
58
61
|
s.add_argument("address")
|
|
@@ -83,7 +86,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
83
86
|
|
|
84
87
|
a = p.parse_args(argv)
|
|
85
88
|
import os
|
|
86
|
-
|
|
89
|
+
record = getattr(a, "record", False) or os.environ.get("DELPHI_RECORD", "").lower() in ("1", "true", "yes")
|
|
90
|
+
d = Delphi(a.network or os.environ.get("DELPHI_NETWORK", "mainnet"), record=record if a.cmd == "buy" else False)
|
|
87
91
|
try:
|
|
88
92
|
return _run(d, a)
|
|
89
93
|
except (DelphiError, ValueError) as e:
|
|
@@ -138,8 +142,15 @@ def _run(d: Delphi, a) -> int:
|
|
|
138
142
|
q = d.quote_buy(a.address, a.outcome, usd=a.usd)
|
|
139
143
|
if not _confirm(f"Buy {q.shares:.4f} {q.outcome_label} for ${q.usd:.2f} (max +{a.max_slippage:.0%})?", a.yes):
|
|
140
144
|
return 1
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
belief = {a.outcome: a.belief} if a.belief is not None else None
|
|
146
|
+
t = d.buy(a.address, a.outcome, usd=a.usd, max_slippage=a.max_slippage, belief=belief, reasoning=a.reason)
|
|
147
|
+
if a.json:
|
|
148
|
+
_out(t.to_dict(), True)
|
|
149
|
+
else:
|
|
150
|
+
print(f"bought {t.shares:.4f} {t.outcome_label} for ${t.usd:.4f}\n{t.explorer_url}")
|
|
151
|
+
if t.record and t.record.get("logged"):
|
|
152
|
+
print(f"belief {t.record['belief']:.0%} vs market {t.record['market_probability']:.0%}, sealed until "
|
|
153
|
+
f"{t.record['sealed_until']}\nrecord: {t.record['record_url']}")
|
|
143
154
|
return 0
|
|
144
155
|
if a.cmd == "sell":
|
|
145
156
|
if not _confirm(f"Sell {'all' if a.shares is None else a.shares} {a.outcome} shares?", a.yes):
|
|
@@ -53,7 +53,11 @@ class Delphi:
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
def __init__(self, network: str | Network = "mainnet", *, private_key: str | None = None,
|
|
56
|
-
rpc_url: str | None = None, cache: bool = True
|
|
56
|
+
rpc_url: str | None = None, cache: bool = True, record: bool | str = False,
|
|
57
|
+
recorder_url: str | None = None, agent_name: str | None = None, agent_model: str = ""):
|
|
58
|
+
"""``record=True`` turns on the flight recorder: every buy must state what the agent believes, and that
|
|
59
|
+
belief is sealed and anchored on chain (via Agon) before the trade is sent. No entry, no trade.
|
|
60
|
+
``record="try"`` trades even if the entry can't be logged, and says why on the Trade."""
|
|
57
61
|
self.network = network if isinstance(network, Network) else get_network(network)
|
|
58
62
|
self.w3 = connect(self.network, rpc_url or os.environ.get("DELPHI_RPC_URL"))
|
|
59
63
|
self.reader = Reader(self.w3, self.network)
|
|
@@ -68,6 +72,16 @@ class Delphi:
|
|
|
68
72
|
self._addresses: dict[str, list[str]] = {} # deployment name -> market addresses, oldest first
|
|
69
73
|
self._meta: dict[str, tuple[Optional[dict], Optional[bool]]] = {}
|
|
70
74
|
self._http = httpx.Client(timeout=20, follow_redirects=True, headers={"User-Agent": "delphi-kit"})
|
|
75
|
+
self.recorder = None
|
|
76
|
+
self.record_mode = "off"
|
|
77
|
+
if record:
|
|
78
|
+
if not self.account:
|
|
79
|
+
raise DelphiError("record=True needs a wallet key: entries are signed by the trading wallet")
|
|
80
|
+
if self.network.name != "mainnet":
|
|
81
|
+
raise DelphiError("the flight recorder covers mainnet markets only")
|
|
82
|
+
from .recorder import Recorder
|
|
83
|
+
self.recorder = Recorder(key, url=recorder_url, name=agent_name, model=agent_model, chain_id=self.network.chain_id)
|
|
84
|
+
self.record_mode = "try" if record == "try" else "required"
|
|
71
85
|
|
|
72
86
|
# ------------------------------------------------------------------ wallet
|
|
73
87
|
|
|
@@ -366,12 +380,21 @@ class Delphi:
|
|
|
366
380
|
return "0x" + r["transactionHash"].hex().removeprefix("0x")
|
|
367
381
|
|
|
368
382
|
def buy(self, market: str | Market, outcome: int | str, *, usd: float | None = None, shares: float | None = None,
|
|
369
|
-
max_slippage: float = 0.02, approve_unlimited: bool = False
|
|
383
|
+
max_slippage: float = 0.02, approve_unlimited: bool = False, belief=None, reasoning: str | None = None,
|
|
384
|
+
receipt: str | None = None) -> Trade:
|
|
370
385
|
"""Buy shares of one outcome. ``usd`` spends up to that budget; ``shares`` buys an exact count.
|
|
371
|
-
Approves only the amount needed unless ``approve_unlimited``.
|
|
386
|
+
Approves only the amount needed unless ``approve_unlimited``.
|
|
387
|
+
|
|
388
|
+
With the flight recorder on, ``belief`` is required: the probabilities the agent believes, e.g.
|
|
389
|
+
``{"Yes": 0.71}`` (one side is enough for yes/no markets). ``reasoning`` is sealed with it."""
|
|
372
390
|
acct = self._need_account()
|
|
373
391
|
m, idx = self._resolve(market, outcome)
|
|
374
392
|
q = self.quote_buy(m, idx, usd=usd, shares=shares)
|
|
393
|
+
if self.recorder and belief is None:
|
|
394
|
+
raise DelphiError('the flight recorder is on: say what you believe, e.g. belief={"Yes": 0.7}')
|
|
395
|
+
if self.recorder:
|
|
396
|
+
from .recorder import belief_vector
|
|
397
|
+
belief_vector(m, belief) # bad beliefs fail here, before any approval is sent
|
|
375
398
|
max_in = math.ceil(q.usd_raw * (1 + max_slippage))
|
|
376
399
|
have = self.token.functions.balanceOf(acct.address).call()
|
|
377
400
|
if have < max_in:
|
|
@@ -380,11 +403,30 @@ class Delphi:
|
|
|
380
403
|
approval = self._ensure_allowance(m.address, max_in, approve_unlimited)
|
|
381
404
|
fn = self._gw[m.deployment].functions.buyExactOut(checksum(m.address), idx, q.shares_raw, max_in)
|
|
382
405
|
sim = fn.call({"from": acct.address})
|
|
406
|
+
# log the belief last, once the trade is known to go through, so the record never has entries for trades
|
|
407
|
+
# that failed before sending
|
|
408
|
+
rec = self._log_belief(m, idx, belief, reasoning, receipt) if self.recorder else None
|
|
383
409
|
r = self._send(fn)
|
|
384
410
|
paid = int(sim[0] if isinstance(sim, (list, tuple)) else sim)
|
|
385
411
|
h = "0x" + r["transactionHash"].hex().removeprefix("0x")
|
|
386
412
|
return Trade(market=m.address, outcome=idx, outcome_label=q.outcome_label, side="buy", shares=q.shares,
|
|
387
|
-
usd=paid / 10**self.network.token_decimals, tx_hash=h, explorer_url=self.tx_url(h), approval_tx=approval
|
|
413
|
+
usd=paid / 10**self.network.token_decimals, tx_hash=h, explorer_url=self.tx_url(h), approval_tx=approval,
|
|
414
|
+
record=rec)
|
|
415
|
+
|
|
416
|
+
def _log_belief(self, m: Market, idx: int, belief, reasoning, receipt) -> dict:
|
|
417
|
+
"""Seal the agent's belief before a trade. Raises (so nothing is traded) unless record="try"."""
|
|
418
|
+
from .recorder import RecorderError, belief_vector
|
|
419
|
+
vec = belief_vector(m, belief)
|
|
420
|
+
mine, theirs = vec[idx] / (sum(vec) or 1), (m.probabilities[idx] if idx < len(m.probabilities) else None)
|
|
421
|
+
try:
|
|
422
|
+
e = self.recorder.log(m, vec, reasoning, receipt=receipt)
|
|
423
|
+
except (RecorderError, ValueError) as err:
|
|
424
|
+
if self.record_mode == "try":
|
|
425
|
+
return {"logged": False, "error": str(err)}
|
|
426
|
+
raise DelphiError(f"not traded: the belief could not be recorded ({err})") from None
|
|
427
|
+
return {"logged": True, "entry": e.id, "belief": round(mine, 4), "market_probability": theirs,
|
|
428
|
+
"edge": round(mine - theirs, 4) if theirs is not None else None, "sealed_until": e.sealed_until,
|
|
429
|
+
"record_url": e.url, "proof_url": e.proof_url}
|
|
388
430
|
|
|
389
431
|
def sell(self, market: str | Market, outcome: int | str, shares: float | None = None, *,
|
|
390
432
|
max_slippage: float = 0.02) -> Trade:
|
|
@@ -20,7 +20,10 @@ from .limits import Guard, LimitExceeded, Limits
|
|
|
20
20
|
INSTRUCTIONS = """Delphi is a prediction market on the Gensyn chain, settled by an AI judge that reads each
|
|
21
21
|
market's rules and named sources. Prices are probabilities (0-1). Before buying: read the market's rules and
|
|
22
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.
|
|
23
|
+
trades. Trades are held to spending limits shown by wallet_status; never try to work around them.
|
|
24
|
+
If wallet_status shows the flight recorder is on, every buy must state your honest probability for the outcome
|
|
25
|
+
you buy and a short reason. It is sealed until trading closes and scored against the market when it settles, so
|
|
26
|
+
state what you actually believe, not what justifies the trade."""
|
|
24
27
|
|
|
25
28
|
READ = ToolAnnotations(read_only_hint=True, open_world_hint=True)
|
|
26
29
|
WRITE = ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False, open_world_hint=True)
|
|
@@ -33,7 +36,10 @@ _guard: Optional[Guard] = None
|
|
|
33
36
|
def client() -> Delphi:
|
|
34
37
|
global _client
|
|
35
38
|
if _client is None:
|
|
36
|
-
|
|
39
|
+
rec = os.environ.get("DELPHI_RECORD", "").lower()
|
|
40
|
+
_client = Delphi(os.environ.get("DELPHI_NETWORK", "mainnet"),
|
|
41
|
+
record="try" if rec == "try" else rec in ("1", "true", "yes"),
|
|
42
|
+
agent_name=os.environ.get("DELPHI_AGENT_NAME"), agent_model=os.environ.get("DELPHI_AGENT_MODEL", ""))
|
|
37
43
|
return _client
|
|
38
44
|
|
|
39
45
|
|
|
@@ -115,7 +121,9 @@ def get_positions(wallet: str = "") -> Any:
|
|
|
115
121
|
def wallet_status() -> Any:
|
|
116
122
|
"""The agent's wallet address, USDC and gas balance, and its spending limits and spend so far today."""
|
|
117
123
|
c = client()
|
|
118
|
-
out: dict[str, Any] = {"network": c.network.name, "limits": guard().status()
|
|
124
|
+
out: dict[str, Any] = {"network": c.network.name, "limits": guard().status(),
|
|
125
|
+
"flight_recorder": {"on": True, "mode": c.record_mode, "agent": c.recorder.name,
|
|
126
|
+
"record_url": c.recorder.agent_url} if c.recorder else {"on": False}}
|
|
119
127
|
if c.address:
|
|
120
128
|
try:
|
|
121
129
|
out.update(c.balance())
|
|
@@ -128,13 +136,21 @@ def wallet_status() -> Any:
|
|
|
128
136
|
|
|
129
137
|
|
|
130
138
|
@server.tool(annotations=WRITE)
|
|
131
|
-
def buy(market: str, outcome: str, usd: float) -> Any:
|
|
139
|
+
def buy(market: str, outcome: str, usd: float, probability: float = -1, reasoning: str = "") -> Any:
|
|
132
140
|
"""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.
|
|
141
|
+
Quote first and read the rules; this sends a real transaction.
|
|
142
|
+
probability: your honest probability (0-1) that `outcome` wins. Required when the flight recorder is on;
|
|
143
|
+
it is sealed with `reasoning` before the trade and scored after the market settles."""
|
|
134
144
|
g = guard()
|
|
135
145
|
try:
|
|
136
146
|
g.check(market, usd)
|
|
137
|
-
|
|
147
|
+
c = client()
|
|
148
|
+
belief = None
|
|
149
|
+
if c.recorder:
|
|
150
|
+
if not 0 < probability < 1:
|
|
151
|
+
raise ValueError("the flight recorder is on: pass probability, your honest chance (0-1) that this outcome wins")
|
|
152
|
+
belief = {outcome: probability}
|
|
153
|
+
t = c.buy(market, outcome, usd=usd, max_slippage=g.limits.max_slippage, belief=belief, reasoning=reasoning or None)
|
|
138
154
|
g.record(t.usd)
|
|
139
155
|
return t.to_dict()
|
|
140
156
|
except (LimitExceeded, DelphiError, ValueError) as e:
|
|
@@ -172,6 +188,19 @@ def collect(market: str) -> Any:
|
|
|
172
188
|
return _fail(e)
|
|
173
189
|
|
|
174
190
|
|
|
191
|
+
@server.tool(annotations=READ)
|
|
192
|
+
def my_record() -> Any:
|
|
193
|
+
"""The agent's flight record: every sealed belief it logged before trading, and once markets settle, how its
|
|
194
|
+
probabilities scored against the market's. Sealed entries show no numbers until trading closes."""
|
|
195
|
+
c = client()
|
|
196
|
+
if not c.recorder:
|
|
197
|
+
return {"on": False, "note": "The flight recorder is off. Set DELPHI_RECORD=true to turn it on."}
|
|
198
|
+
try:
|
|
199
|
+
return {"record_url": c.recorder.agent_url, **c.recorder.record()}
|
|
200
|
+
except Exception as e: # noqa: BLE001
|
|
201
|
+
return _fail(e)
|
|
202
|
+
|
|
203
|
+
|
|
175
204
|
def main() -> None:
|
|
176
205
|
server.run("stdio")
|
|
177
206
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Flight recorder: before an agent trades, it writes down what it believes, and that entry is sealed and anchored
|
|
2
|
+
on the Gensyn chain by Agon (https://agon.nerom.site, open source at github.com/neromtoobad/agon).
|
|
3
|
+
|
|
4
|
+
When the market settles, the agent's record writes itself:
|
|
5
|
+
|
|
6
|
+
- calibration: were the stated probabilities better than the market price at the time? (Agon's scoring)
|
|
7
|
+
- results: what the same wallet actually bought and sold, read from the chain
|
|
8
|
+
|
|
9
|
+
The entry is signed with the trading wallet's own key (EIP-712), so the record and the trades share one address.
|
|
10
|
+
Entries are sealed until trading closes: nobody, including Agon, can show or change them before then.
|
|
11
|
+
|
|
12
|
+
d = Delphi(private_key=key, record=True)
|
|
13
|
+
d.buy(m, "Yes", usd=2, belief={"Yes": 0.71}, reasoning="Polls moved after the debate ...")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
from eth_account import Account
|
|
25
|
+
from eth_account.messages import encode_typed_data
|
|
26
|
+
from web3 import Web3
|
|
27
|
+
|
|
28
|
+
from .models import Market, _Dict
|
|
29
|
+
|
|
30
|
+
DEFAULT_URL = "https://agon.nerom.site"
|
|
31
|
+
ZERO32 = "0x" + "00" * 32
|
|
32
|
+
MAX_REASONING = 4000
|
|
33
|
+
|
|
34
|
+
TYPES = {
|
|
35
|
+
"Agent": [
|
|
36
|
+
{"name": "name", "type": "string"}, {"name": "uri", "type": "string"}, {"name": "client", "type": "string"},
|
|
37
|
+
{"name": "model", "type": "string"}, {"name": "issuedAt", "type": "uint64"},
|
|
38
|
+
],
|
|
39
|
+
"Forecast": [
|
|
40
|
+
{"name": "market", "type": "address"}, {"name": "probabilities", "type": "uint16[]"},
|
|
41
|
+
{"name": "issuedAt", "type": "uint64"}, {"name": "visibility", "type": "uint8"},
|
|
42
|
+
{"name": "reasoningHash", "type": "bytes32"}, {"name": "receiptHash", "type": "bytes32"},
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RecorderError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Entry(_Dict):
|
|
53
|
+
"""One sealed forecast in the agent's record."""
|
|
54
|
+
id: int
|
|
55
|
+
market: str
|
|
56
|
+
probabilities: list[float]
|
|
57
|
+
sealed_until: Optional[str]
|
|
58
|
+
url: str
|
|
59
|
+
proof_url: str
|
|
60
|
+
leaf: str = field(repr=False, default="")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def to_basis_points(probs: Sequence[float]) -> list[int]:
|
|
64
|
+
"""Probabilities (any non-negative weights) to whole basis points, each 100-9900, adding up to exactly 10000."""
|
|
65
|
+
if not probs or any(p < 0 for p in probs):
|
|
66
|
+
raise ValueError("probabilities must be non-negative, one per outcome")
|
|
67
|
+
total = float(sum(probs))
|
|
68
|
+
if total <= 0:
|
|
69
|
+
raise ValueError("probabilities can't all be zero")
|
|
70
|
+
n = len(probs)
|
|
71
|
+
if n * 100 > 10000:
|
|
72
|
+
raise ValueError("too many outcomes to record (at most 100)")
|
|
73
|
+
bp = [min(9900, max(100, round(p / total * 10000))) for p in probs]
|
|
74
|
+
# put the rounding error on the largest outcome that still has room, one point at a time
|
|
75
|
+
while (diff := 10000 - sum(bp)) != 0:
|
|
76
|
+
step = 1 if diff > 0 else -1
|
|
77
|
+
bp[max((i for i in range(n) if 100 <= bp[i] + step <= 9900), key=lambda k: bp[k])] += step
|
|
78
|
+
return bp
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def belief_vector(m: Market, belief: Mapping[Any, float] | Sequence[float]) -> list[float]:
|
|
82
|
+
"""Accept {"Yes": 0.7}, {"Yes": 0.7, "No": 0.3}, {0: 0.7} or [0.7, 0.3]. For a two-outcome market, one side is enough."""
|
|
83
|
+
n = len(m.outcomes) or len(m.probabilities)
|
|
84
|
+
if isinstance(belief, Mapping):
|
|
85
|
+
vec: list[Optional[float]] = [None] * n
|
|
86
|
+
for k, v in belief.items():
|
|
87
|
+
vec[m.outcome_index(k)] = float(v)
|
|
88
|
+
missing = [i for i, v in enumerate(vec) if v is None]
|
|
89
|
+
if missing:
|
|
90
|
+
given = sum(v for v in vec if v is not None)
|
|
91
|
+
if given > 1 + 1e-9:
|
|
92
|
+
raise ValueError("stated probabilities add up to more than 1")
|
|
93
|
+
share = (1 - given) / len(missing)
|
|
94
|
+
for i in missing:
|
|
95
|
+
vec[i] = share
|
|
96
|
+
out = [float(v) for v in vec] # type: ignore[arg-type]
|
|
97
|
+
else:
|
|
98
|
+
out = [float(v) for v in belief]
|
|
99
|
+
if len(out) != n:
|
|
100
|
+
raise ValueError(f"this market has {n} outcomes, got {len(out)} probabilities")
|
|
101
|
+
if any(not 0 <= v <= 1 for v in out) or abs(sum(out) - 1) > 0.02:
|
|
102
|
+
raise ValueError(f"stated probabilities must each be 0-1 and add up to 1 (these add up to {sum(out):.2f})")
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Recorder:
|
|
107
|
+
"""Talks to an Agon server. ``Delphi(record=True)`` makes one for you."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, private_key: str, *, url: str | None = None, name: str | None = None, model: str = "",
|
|
110
|
+
uri: str = "", chain_id: int = 685689, timeout: float = 30):
|
|
111
|
+
self.account = Account.from_key(private_key)
|
|
112
|
+
self.url = (url or os.environ.get("DELPHI_RECORDER_URL") or DEFAULT_URL).rstrip("/")
|
|
113
|
+
self.name = name or os.environ.get("DELPHI_AGENT_NAME") or f"agent-{self.account.address[2:8].lower()}"
|
|
114
|
+
self.model = model or os.environ.get("DELPHI_AGENT_MODEL", "")
|
|
115
|
+
self.uri = uri
|
|
116
|
+
self.domain = {"name": "Agon", "version": "1", "chainId": chain_id}
|
|
117
|
+
self.http = httpx.Client(timeout=timeout, headers={"user-agent": "delphi-kit"})
|
|
118
|
+
self._registered = False
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def address(self) -> str:
|
|
122
|
+
return self.account.address
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def agent_url(self) -> str:
|
|
126
|
+
return f"{self.url}/#/agent/{self.address.lower()}"
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------ signing
|
|
129
|
+
|
|
130
|
+
def _sign(self, primary: str, message: dict) -> str:
|
|
131
|
+
msg = encode_typed_data(full_message={
|
|
132
|
+
"types": {"EIP712Domain": [{"name": "name", "type": "string"}, {"name": "version", "type": "string"},
|
|
133
|
+
{"name": "chainId", "type": "uint256"}],
|
|
134
|
+
# only the primary type: Agent and Forecast don't reference each other, so the hash is the
|
|
135
|
+
# same, and eth_account refuses messages whose types have more than one possible root
|
|
136
|
+
primary: TYPES[primary]},
|
|
137
|
+
"primaryType": primary, "domain": self.domain, "message": message})
|
|
138
|
+
sig = self.account.sign_message(msg).signature.hex()
|
|
139
|
+
return sig if sig.startswith("0x") else "0x" + sig
|
|
140
|
+
|
|
141
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
142
|
+
try:
|
|
143
|
+
r = self.http.post(self.url + path, json=body)
|
|
144
|
+
except httpx.HTTPError as e:
|
|
145
|
+
raise RecorderError(f"can't reach the recorder at {self.url}: {e}") from None
|
|
146
|
+
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
|
|
147
|
+
if r.status_code >= 400:
|
|
148
|
+
raise RecorderError(data.get("error") or f"recorder answered HTTP {r.status_code}")
|
|
149
|
+
return data
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------ API
|
|
152
|
+
|
|
153
|
+
def register(self) -> dict:
|
|
154
|
+
"""Register (or update) this wallet as an agent. Called for you before the first entry."""
|
|
155
|
+
msg = {"name": self.name, "uri": self.uri, "client": "delphi-kit", "model": self.model, "issuedAt": int(time.time())}
|
|
156
|
+
out = self._post("/league/v1/agents", {"address": self.address, "message": msg, "signature": self._sign("Agent", msg)})
|
|
157
|
+
self._registered = True
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
def _ensure_registered(self) -> None:
|
|
161
|
+
if self._registered:
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
r = self.http.get(f"{self.url}/league/v1/agents/{self.address.lower()}")
|
|
165
|
+
if r.status_code == 200:
|
|
166
|
+
self._registered = True
|
|
167
|
+
return
|
|
168
|
+
except httpx.HTTPError:
|
|
169
|
+
pass
|
|
170
|
+
self.register()
|
|
171
|
+
|
|
172
|
+
def log(self, market: Market, belief: Mapping[Any, float] | Sequence[float], reasoning: str | None = None,
|
|
173
|
+
*, public: bool = False, receipt: str | None = None) -> Entry:
|
|
174
|
+
"""Seal what the agent believes about ``market`` now. ``reasoning`` is stored and its hash signed; it is
|
|
175
|
+
revealed with the forecast when trading closes. ``receipt`` is an optional REE receipt file's text."""
|
|
176
|
+
self._ensure_registered()
|
|
177
|
+
bp = to_basis_points(belief_vector(market, belief))
|
|
178
|
+
if reasoning is not None and len(reasoning) > MAX_REASONING:
|
|
179
|
+
reasoning = reasoning[:MAX_REASONING]
|
|
180
|
+
rh = Web3.keccak(text=reasoning).hex() if reasoning else ZERO32
|
|
181
|
+
rh = rh if rh.startswith("0x") else "0x" + rh
|
|
182
|
+
receipt_hash = ZERO32
|
|
183
|
+
if receipt is not None:
|
|
184
|
+
import json
|
|
185
|
+
receipt_hash = "0x" + json.loads(receipt)["hashes"]["receipt_hash"].removeprefix("0x").lower()
|
|
186
|
+
msg = {"market": Web3.to_checksum_address(market.address), "probabilities": bp, "issuedAt": int(time.time()),
|
|
187
|
+
"visibility": 1 if public else 0, "reasoningHash": rh, "receiptHash": receipt_hash}
|
|
188
|
+
body: dict[str, Any] = {"address": self.address, "message": msg, "signature": self._sign("Forecast", msg)}
|
|
189
|
+
if reasoning:
|
|
190
|
+
body["reasoning"] = reasoning
|
|
191
|
+
if receipt is not None:
|
|
192
|
+
body["receipt"] = receipt
|
|
193
|
+
out = self._post("/league/v1/forecasts", body)
|
|
194
|
+
return Entry(id=out["id"], market=market.address, probabilities=[b / 10000 for b in bp],
|
|
195
|
+
sealed_until=out.get("sealed_until"), url=self.agent_url,
|
|
196
|
+
proof_url=f"{self.url}/league/v1/forecasts/{out['id']}/proof", leaf=out.get("leaf", ""))
|
|
197
|
+
|
|
198
|
+
def record(self) -> dict:
|
|
199
|
+
"""The agent's record so far: points, scored markets, and every entry (sealed ones show no numbers)."""
|
|
200
|
+
r = self.http.get(f"{self.url}/league/v1/agents/{self.address.lower()}")
|
|
201
|
+
if r.status_code == 404:
|
|
202
|
+
return {"address": self.address, "registered": False, "forecasts": []}
|
|
203
|
+
r.raise_for_status()
|
|
204
|
+
return r.json()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|