delphi-kit 0.1.0__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.
Files changed (23) hide show
  1. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/PKG-INFO +36 -2
  2. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/README.md +35 -1
  3. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/pyproject.toml +1 -1
  4. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/pyproject.toml.orig +1 -1
  5. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/__init__.py +1 -1
  6. delphi_kit-0.2.0/src/delphi_kit/abi/LmsrGatewayV1.json +1 -0
  7. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/chain.py +1 -1
  8. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/cli.py +14 -3
  9. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/client.py +47 -5
  10. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/mcp_server.py +35 -6
  11. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/models.py +1 -0
  12. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/networks.py +2 -2
  13. delphi_kit-0.2.0/src/delphi_kit/recorder.py +204 -0
  14. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/DelphiFactory.json +0 -0
  15. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/DpmGateway.json +0 -0
  16. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/DpmGatewayLegacy.json +0 -0
  17. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/LmsrGateway.json +0 -0
  18. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/LmsrMarket.json +0 -0
  19. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/abi/__init__.py +0 -0
  20. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/limits.py +0 -0
  21. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/py.typed +0 -0
  22. {delphi_kit-0.1.0 → delphi_kit-0.2.0}/src/delphi_kit/search.py +0 -0
  23. {delphi_kit-0.1.0 → 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.1.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 `wallet_status`.
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 `wallet_status`.
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.1.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.1.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"
@@ -6,4 +6,4 @@ from .networks import NETWORKS, Deployment, Network
6
6
 
7
7
  __all__ = ["Delphi", "DelphiError", "InsufficientFunds", "Market", "Position", "Quote", "Trade",
8
8
  "NETWORKS", "Network", "Deployment"]
9
- __version__ = "0.1.0"
9
+ __version__ = "0.2.0"
@@ -0,0 +1 @@
1
+ [{"inputs": [{"internalType": "contract IERC20Metadata", "name": "token_", "type": "address"}, {"internalType": "address", "name": "owner_", "type": "address"}], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [{"internalType": "uint256", "name": "allowance", "type": "uint256"}, {"internalType": "uint256", "name": "maxTokensIn", "type": "uint256"}], "name": "AllowanceTooLow", "type": "error"}, {"inputs": [], "name": "BuyTooSmall", "type": "error"}, {"inputs": [{"internalType": "address", "name": "caller", "type": "address"}], "name": "CallerIsNotOracleRelayer", "type": "error"}, {"inputs": [{"internalType": "address", "name": "delphiFactory", "type": "address"}], "name": "DelphiFactoryIsNotContract", "type": "error"}, {"inputs": [], "name": "DelphiFactoryIsZeroAddress", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "expInput", "type": "uint256"}, {"internalType": "uint256", "name": "maxExpInput", "type": "uint256"}], "name": "ExpInputTooBig", "type": "error"}, {"inputs": [], "name": "GatewayNotInitialized", "type": "error"}, {"inputs": [{"internalType": "address", "name": "initializer", "type": "address"}, {"internalType": "address", "name": "deployer", "type": "address"}], "name": "InitializerNotDeployer", "type": "error"}, {"inputs": [], "name": "InvalidExponent", "type": "error"}, {"inputs": [], "name": "InvalidInitialization", "type": "error"}, {"inputs": [], "name": "LnInputTooSmall", "type": "error"}, {"inputs": [], "name": "MarketNotOpen", "type": "error"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}], "name": "MarketProxyNotDeployedByFactory", "type": "error"}, {"inputs": [], "name": "NotInitializing", "type": "error"}, {"inputs": [{"internalType": "address", "name": "oracleRelayer", "type": "address"}], "name": "OracleRelayerIsNotAContract", "type": "error"}, {"inputs": [], "name": "OracleRelayerIsZeroAddress", "type": "error"}, {"inputs": [], "name": "OracleRelayerNotSet", "type": "error"}, {"inputs": [], "name": "OutOfBounds", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "newSupply", "type": "uint256"}, {"internalType": "uint256", "name": "minDelta", "type": "uint256"}], "name": "OutcomeNewSupplyBelowMinDelta", "type": "error"}, {"inputs": [{"internalType": "address", "name": "owner", "type": "address"}], "name": "OwnableInvalidOwner", "type": "error"}, {"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "OwnableUnauthorizedAccount", "type": "error"}, {"inputs": [], "name": "RatioTooSmall", "type": "error"}, {"inputs": [{"internalType": "int256", "name": "value", "type": "int256"}], "name": "SafeCastOverflowedIntToUint", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "value", "type": "uint256"}], "name": "SafeCastOverflowedUintToInt", "type": "error"}, {"inputs": [], "name": "SellTooSmall", "type": "error"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}], "name": "SettlementAlreadyLocked", "type": "error"}, {"inputs": [], "name": "SettlementNotLocked", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"internalType": "uint256", "name": "minDelta", "type": "uint256"}], "name": "SharesInBelowMinDelta", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"internalType": "uint256", "name": "supply", "type": "uint256"}], "name": "SharesInExceedSupply", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "sharesOut", "type": "uint256"}, {"internalType": "uint256", "name": "minDelta", "type": "uint256"}], "name": "SharesOutBelowMinDelta", "type": "error"}, {"inputs": [{"internalType": "uint8", "name": "decimals", "type": "uint8"}], "name": "TokenDecimalsAreTooHigh", "type": "error"}, {"inputs": [{"internalType": "uint8", "name": "decimals", "type": "uint8"}], "name": "TokenDecimalsAreTooLow", "type": "error"}, {"inputs": [{"internalType": "address", "name": "token", "type": "address"}], "name": "TokenIsNotAContract", "type": "error"}, {"inputs": [], "name": "TokenIsZeroAddress", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "minTokensIn", "type": "uint256"}], "name": "TokensInBelowMinTokensDelta", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "maxTokensIn", "type": "uint256"}], "name": "TokensInExceedMaxTokensIn", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "tokensOut", "type": "uint256"}, {"internalType": "uint256", "name": "minTokensOut", "type": "uint256"}], "name": "TokensOutBelowMinTokensDelta", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "tokensOut", "type": "uint256"}, {"internalType": "uint256", "name": "minTokensOut", "type": "uint256"}], "name": "TokensOutBelowMinTokensOut", "type": "error"}, {"inputs": [], "name": "ZeroSharesIn", "type": "error"}, {"inputs": [], "name": "ZeroSharesOut", "type": "error"}, {"inputs": [], "name": "ZeroTokensIn", "type": "error"}, {"inputs": [], "name": "ZeroTokensOut", "type": "error"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "sharesOut", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "name": "GatewayBuy", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"indexed": true, "internalType": "address", "name": "liquidator", "type": "address"}, {"indexed": false, "internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}, {"indexed": false, "internalType": "uint256[]", "name": "sharesIn", "type": "uint256[]"}, {"indexed": false, "internalType": "uint256", "name": "totalTokensOut", "type": "uint256"}], "name": "GatewayLiquidation", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "marketProxy", "type": "address"}], "name": "GatewayMarketFailed", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "marketProxy", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "winningOutcomeIdx", "type": "uint256"}], "name": "GatewayMarketSettled", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"indexed": true, "internalType": "address", "name": "redeemer", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "tokensOut", "type": "uint256"}], "name": "GatewayRedemption", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "tokensOut", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "name": "GatewaySell", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "uint64", "name": "version", "type": "uint64"}], "name": "Initialized", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "marketProxy", "type": "address"}, {"indexed": true, "internalType": "address", "name": "keeper", "type": "address"}], "name": "MarketResolutionRequested", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "previousRelayer", "type": "address"}, {"indexed": true, "internalType": "address", "name": "newRelayer", "type": "address"}], "name": "OracleRelayerSet", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"}, {"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"}], "name": "OwnershipTransferStarted", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"}, {"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"}], "name": "OwnershipTransferred", "type": "event"}, {"inputs": [], "name": "MIN_SHARES_DELTA", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "MIN_TOKENS_DELTA", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "TOKEN", "outputs": [{"internalType": "contract IERC20Metadata", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "TOKEN_DECIMAL_SCALER", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "acceptOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "id", "type": "uint256"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "address[]", "name": "owners", "type": "address[]"}, {"internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}], "name": "batchBalanceOf", "outputs": [{"internalType": "uint256[]", "name": "", "type": "uint256[]"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"internalType": "uint256", "name": "sharesOut", "type": "uint256"}, {"internalType": "uint256", "name": "maxTokensIn", "type": "uint256"}], "name": "buyExactOut", "outputs": [{"internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"internalType": "uint256", "name": "sharesOut", "type": "uint256"}, {"internalType": "uint256", "name": "maxTokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint8", "name": "v", "type": "uint8"}, {"internalType": "bytes32", "name": "r", "type": "bytes32"}, {"internalType": "bytes32", "name": "s", "type": "bytes32"}], "name": "buyExactOutWithPermit", "outputs": [{"internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "delphiFactory", "outputs": [{"internalType": "contract IDelphiFactory", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}], "name": "failMarket", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "getMarket", "outputs": [{"components": [{"components": [{"internalType": "uint256", "name": "outcomeCount", "type": "uint256"}, {"internalType": "uint256", "name": "b", "type": "uint256"}, {"internalType": "uint256", "name": "tradingFee", "type": "uint256"}, {"internalType": "uint256", "name": "tradingDeadline", "type": "uint256"}, {"internalType": "uint256", "name": "settlementDeadline", "type": "uint256"}], "internalType": "struct ILmsrMarketTypes.MarketConfig", "name": "config", "type": "tuple"}, {"internalType": "uint256", "name": "pool", "type": "uint256"}, {"internalType": "uint256", "name": "tradingFees", "type": "uint256"}, {"internalType": "uint256", "name": "expSum", "type": "uint256"}, {"internalType": "uint256", "name": "winningOutcomeIdx", "type": "uint256"}], "internalType": "struct ILmsrMarketTypes.Market", "name": "", "type": "tuple"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract IDelphiFactory", "name": "delphiFactory_", "type": "address"}], "name": "initialize", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "isOperator", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}], "name": "liquidate", "outputs": [{"internalType": "uint256[]", "name": "sharesIn", "type": "uint256[]"}, {"internalType": "uint256", "name": "totalTokensOut", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "marketCreator", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "marketMetadata", "outputs": [{"components": [{"internalType": "string", "name": "uri", "type": "string"}, {"internalType": "bytes32", "name": "uriContentHash", "type": "bytes32"}], "internalType": "struct IDelphiMarket.VerifiableUri", "name": "", "type": "tuple"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "marketStatus", "outputs": [{"internalType": "enum ILmsrMarketTypes.MarketStatus", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "maxB", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "maxOutcomeCount", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "maxSettlementWindow", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "maxTradingFee", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "maxTradingWindow", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "minB", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "minOutcomeCount", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "minSettlementWindow", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "minTradingFee", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "minTradingWindow", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "oracleRelayer", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "outcomeSuppliesSum", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "owner", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "pendingOwner", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"internalType": "uint256", "name": "sharesOut", "type": "uint256"}], "name": "quoteBuyExactOut", "outputs": [{"internalType": "uint256", "name": "tokensIn", "type": "uint256"}, {"internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"internalType": "uint256", "name": "sharesIn", "type": "uint256"}], "name": "quoteSellExactIn", "outputs": [{"internalType": "uint256", "name": "tokensOut", "type": "uint256"}, {"internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "redeem", "outputs": [{"internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"internalType": "uint256", "name": "tokensOut", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}], "name": "resolveMarket", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}, {"internalType": "uint256", "name": "sharesIn", "type": "uint256"}, {"internalType": "uint256", "name": "minTokensOut", "type": "uint256"}], "name": "sellExactIn", "outputs": [{"internalType": "uint256", "name": "tokensOut", "type": "uint256"}, {"internalType": "uint256", "name": "outcomeNewExp", "type": "uint256"}, {"internalType": "uint256", "name": "newExpSum", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "oracleRelayer_", "type": "address"}], "name": "setOracleRelayer", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "winningOutcomeIdx", "type": "uint256"}, {"internalType": "address", "name": "oracleFeeRecipient", "type": "address"}], "name": "settleMarket", "outputs": [{"internalType": "uint256", "name": "losingPayout", "type": "uint256"}, {"internalType": "uint256", "name": "tradingFeesRecipientTradingFeesCut", "type": "uint256"}, {"internalType": "uint256", "name": "marketCreatorTradingFeesCut", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "marketProxy", "type": "address"}], "name": "settlementLocked", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}], "name": "spotImpliedProbabilities", "outputs": [{"internalType": "uint256[]", "name": "", "type": "uint256[]"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}], "name": "spotImpliedProbability", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}], "name": "spotPrice", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}], "name": "spotPrices", "outputs": [{"internalType": "uint256[]", "name": "", "type": "uint256[]"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256[]", "name": "outcomeIndices", "type": "uint256[]"}], "name": "totalSupplies", "outputs": [{"internalType": "uint256[]", "name": "", "type": "uint256[]"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}, {"internalType": "uint256", "name": "outcomeIdx", "type": "uint256"}], "name": "totalSupply", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "newOwner", "type": "address"}], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "contract ILmsrMarket", "name": "marketProxy", "type": "address"}], "name": "trySweep", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
@@ -33,7 +33,7 @@ MULTICALL3_ABI = [
33
33
  {"name": "success", "type": "bool"}, {"name": "returnData", "type": "bytes"}]}]},
34
34
  ]
35
35
 
36
- ABI_FILE = {"lmsr": "LmsrGateway.json", "dpm": "DpmGateway.json", "dpm_legacy": "DpmGatewayLegacy.json",
36
+ ABI_FILE = {"lmsr": "LmsrGateway.json", "lmsr_v1": "LmsrGatewayV1.json", "dpm": "DpmGateway.json", "dpm_legacy": "DpmGatewayLegacy.json",
37
37
  "factory": "DelphiFactory.json", "lmsr_market": "LmsrMarket.json"}
38
38
 
39
39
 
@@ -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
- d = Delphi(a.network or os.environ.get("DELPHI_NETWORK", "mainnet"))
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
- t = d.buy(a.address, a.outcome, usd=a.usd, max_slippage=a.max_slippage)
142
- _out(t.to_dict(), a.json) if a.json else print(f"bought {t.shares:.4f} {t.outcome_label} for ${t.usd:.4f}\n{t.explorer_url}")
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
 
@@ -183,7 +197,7 @@ class Delphi:
183
197
  out.append(Market(
184
198
  address=checksum(a),
185
199
  deployment=dep.name,
186
- mechanism="lmsr" if dep.kind == "lmsr" else "dpm",
200
+ mechanism="lmsr" if dep.kind.startswith("lmsr") else "dpm",
187
201
  status=STATUSES[st] if isinstance(st, int) else "unknown",
188
202
  question=(meta or {}).get("question"),
189
203
  outcomes=outcomes if len(outcomes) == n else (outcomes or [f"Outcome {j}" for j in range(n)]),
@@ -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) -> Trade:
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
- _client = Delphi(os.environ.get("DELPHI_NETWORK", "mainnet"))
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
- t = client().buy(market, outcome, usd=usd, max_slippage=g.limits.max_slippage)
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
 
@@ -102,6 +102,7 @@ class Trade(_Dict):
102
102
  tx_hash: str
103
103
  explorer_url: str
104
104
  approval_tx: Optional[str] = None
105
+ record: Optional[dict] = None # the sealed entry logged before this trade (record=True), or why it wasn't
105
106
 
106
107
 
107
108
  @dataclass
@@ -15,7 +15,7 @@ from dataclasses import dataclass, field
15
15
  @dataclass(frozen=True)
16
16
  class Deployment:
17
17
  name: str # short label, e.g. "lmsr"
18
- kind: str # "lmsr" | "dpm" | "dpm_legacy" (selects the ABI)
18
+ kind: str # "lmsr" | "lmsr_v1" | "dpm" | "dpm_legacy" (selects the ABI)
19
19
  factory: str
20
20
  gateway: str
21
21
  note: str = ""
@@ -84,7 +84,7 @@ NETWORKS: dict[str, Network] = {
84
84
  explorer_url="https://gensyn-testnet.explorer.alchemy.com",
85
85
  app_url="https://agent-competition.gensyn.ai",
86
86
  deployments=(
87
- Deployment("lmsr", "lmsr", "0xEa9D0a78d0209916e88e363B8FDa3e23206Ff49b",
87
+ Deployment("lmsr", "lmsr_v1", "0xEa9D0a78d0209916e88e363B8FDa3e23206Ff49b",
88
88
  "0x097599c9D966fF496284b892A8F13BF885b258ef", "Agent competition."),
89
89
  ),
90
90
  ),
@@ -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()