alloc-context 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alloc_context-0.1.0.dist-info/METADATA +154 -0
- alloc_context-0.1.0.dist-info/RECORD +85 -0
- alloc_context-0.1.0.dist-info/WHEEL +5 -0
- alloc_context-0.1.0.dist-info/entry_points.txt +4 -0
- alloc_context-0.1.0.dist-info/licenses/LICENSE +21 -0
- alloc_context-0.1.0.dist-info/top_level.txt +1 -0
- alloccontext/__init__.py +3 -0
- alloccontext/__main__.py +149 -0
- alloccontext/config.py +415 -0
- alloccontext/horizon.py +30 -0
- alloccontext/ingest/__init__.py +1 -0
- alloccontext/ingest/cf_benchmarks.py +38 -0
- alloccontext/ingest/cf_history.py +65 -0
- alloccontext/ingest/coinbase_client.py +234 -0
- alloccontext/ingest/coinbase_portfolio.py +53 -0
- alloccontext/ingest/coingecko.py +148 -0
- alloccontext/ingest/coinmarketcap.py +135 -0
- alloccontext/ingest/env_keys.py +12 -0
- alloccontext/ingest/etf_flows.py +282 -0
- alloccontext/ingest/exchange/__init__.py +4 -0
- alloccontext/ingest/exchange/coinbase_adapter.py +64 -0
- alloccontext/ingest/exchange/kraken_adapter.py +66 -0
- alloccontext/ingest/exchange/live.py +95 -0
- alloccontext/ingest/exchange/portfolio.py +8 -0
- alloccontext/ingest/exchange/registry.py +27 -0
- alloccontext/ingest/exchange/types.py +5 -0
- alloccontext/ingest/exchange_http.py +28 -0
- alloccontext/ingest/fear_greed.py +89 -0
- alloccontext/ingest/fred.py +138 -0
- alloccontext/ingest/http_errors.py +29 -0
- alloccontext/ingest/kalshi.py +84 -0
- alloccontext/ingest/kalshi_api.py +199 -0
- alloccontext/ingest/kalshi_client.py +95 -0
- alloccontext/ingest/kalshi_files.py +44 -0
- alloccontext/ingest/kalshi_state.py +67 -0
- alloccontext/ingest/kraken_client.py +177 -0
- alloccontext/ingest/kraken_portfolio.py +161 -0
- alloccontext/ingest/macro_calendar.py +310 -0
- alloccontext/ingest/macro_normalize.py +98 -0
- alloccontext/ingest/market_snapshots.py +113 -0
- alloccontext/ingest/outcome.py +110 -0
- alloccontext/ingest/parse_helpers.py +23 -0
- alloccontext/ingest/runner.py +148 -0
- alloccontext/mcp/__init__.py +1 -0
- alloccontext/mcp/assets.py +153 -0
- alloccontext/mcp/bazaar.py +630 -0
- alloccontext/mcp/contracts.py +286 -0
- alloccontext/mcp/handlers.py +487 -0
- alloccontext/mcp/http.py +250 -0
- alloccontext/mcp/payment_middleware.py +211 -0
- alloccontext/mcp/server.py +319 -0
- alloccontext/mcp/staleness.py +30 -0
- alloccontext/mcp/validation.py +56 -0
- alloccontext/mcp/x402_bazaar_dynamic.py +104 -0
- alloccontext/mcp/x402_config.py +131 -0
- alloccontext/mcp/x402_pricing.py +55 -0
- alloccontext/mcp/x402_stables.py +179 -0
- alloccontext/rollup/__init__.py +1 -0
- alloccontext/rollup/band.py +50 -0
- alloccontext/rollup/breadth.py +45 -0
- alloccontext/rollup/cf_math.py +103 -0
- alloccontext/rollup/cluster.py +149 -0
- alloccontext/rollup/cluster_config.py +86 -0
- alloccontext/rollup/comparison.py +67 -0
- alloccontext/rollup/context.py +118 -0
- alloccontext/rollup/delta.py +109 -0
- alloccontext/rollup/etf.py +113 -0
- alloccontext/rollup/fear_greed.py +61 -0
- alloccontext/rollup/macro.py +185 -0
- alloccontext/rollup/portfolio.py +137 -0
- alloccontext/rollup/rebalance.py +125 -0
- alloccontext/rollup/regime.py +188 -0
- alloccontext/rollup/sentiment.py +118 -0
- alloccontext/rollup/snapshots.py +64 -0
- alloccontext/rollup/tape.py +176 -0
- alloccontext/status_report.py +321 -0
- alloccontext/store/__init__.py +0 -0
- alloccontext/store/db.py +216 -0
- alloccontext/store/jsonutil.py +10 -0
- alloccontext/store/meta.py +20 -0
- alloccontext/store/retention.py +63 -0
- alloccontext/store/status.py +89 -0
- alloccontext/timeutil.py +11 -0
- alloccontext/x402_production_check.py +193 -0
- alloccontext/x402_smoke_redact.py +41 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from x402.extensions.bazaar import (
|
|
7
|
+
DeclareMcpDiscoveryConfig,
|
|
8
|
+
OutputConfig,
|
|
9
|
+
declare_discovery_extension,
|
|
10
|
+
declare_mcp_discovery_extension,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
SERVICE_NAME = "AllocContext"
|
|
14
|
+
SERVICE_TITLE = (
|
|
15
|
+
"AllocContext — BTC/ETH allocation drift, rebalance moves & market context"
|
|
16
|
+
)
|
|
17
|
+
# CDP Bazaar indexes service_name (≤32 chars) and up to five tags from payments.
|
|
18
|
+
BAZAAR_SERVICE_NAME = "AllocContext BTC/ETH MCP"
|
|
19
|
+
SERVICE_TAGS = (
|
|
20
|
+
"btc",
|
|
21
|
+
"eth",
|
|
22
|
+
"rebalance",
|
|
23
|
+
"allocation",
|
|
24
|
+
"crypto",
|
|
25
|
+
"portfolio",
|
|
26
|
+
"drift",
|
|
27
|
+
"bitcoin",
|
|
28
|
+
"agent-tools",
|
|
29
|
+
"mcp",
|
|
30
|
+
"sentiment",
|
|
31
|
+
"macro",
|
|
32
|
+
)
|
|
33
|
+
BAZAAR_INDEX_TAGS = ("btc", "eth", "rebalance", "allocation", "portfolio")
|
|
34
|
+
|
|
35
|
+
DISCOVERY_KEYWORD_MARKERS = (
|
|
36
|
+
"portfolio allocation",
|
|
37
|
+
"allocation drift",
|
|
38
|
+
"rebalance plan",
|
|
39
|
+
"fear and greed",
|
|
40
|
+
"etf flows",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
LISTING_DESCRIPTION = (
|
|
44
|
+
"Deterministic BTC/ETH portfolio allocation context for AI agents: "
|
|
45
|
+
"allocation drift and band checks, USD rebalance plans, fused market "
|
|
46
|
+
"context (Fear & Greed, Kalshi sentiment, macro calendar, FRED "
|
|
47
|
+
"indicators, ETF flows), live Kraken/Coinbase portfolio reads, and "
|
|
48
|
+
"saved ContextBundle snapshots. Structured JSON only — no LLM."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_ASSET_FILTER_SCHEMA = {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {"type": "string", "enum": ["BTC", "ETH", "CASH"]},
|
|
54
|
+
"description": "Subset market and ETF fields (default BTC and ETH).",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_TARGET_PCT_SCHEMA = {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Target weights keyed by BTC, ETH, CASH.",
|
|
60
|
+
"properties": {
|
|
61
|
+
"BTC": {"type": "number"},
|
|
62
|
+
"ETH": {"type": "number"},
|
|
63
|
+
"CASH": {"type": "number"},
|
|
64
|
+
},
|
|
65
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_BAND_SCHEMA = {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"description": "Drift band width (for example 0.15 = 15%).",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_MCP_TOOLS: tuple[dict[str, Any], ...] = (
|
|
74
|
+
{
|
|
75
|
+
"tool_name": "get_market_context",
|
|
76
|
+
"description": (
|
|
77
|
+
"Fused market backdrop for BTC/ETH allocation: Fear & Greed, Kalshi "
|
|
78
|
+
"sentiment, macro calendar, FRED indicators, ETF flows, and breadth. "
|
|
79
|
+
"Use freshness=cached for hosted cache; freshness=live runs ingest "
|
|
80
|
+
"first (requires ingest API keys on the host)."
|
|
81
|
+
),
|
|
82
|
+
"input_schema": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"scope": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"enum": ["daily", "weekly"],
|
|
88
|
+
"description": "Rollup horizon for macro and context bundle.",
|
|
89
|
+
},
|
|
90
|
+
"freshness": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"enum": ["cached", "live"],
|
|
93
|
+
"description": (
|
|
94
|
+
"cached reads the ingest DB; live runs ingest first "
|
|
95
|
+
"(requires ingest API keys on the host)."
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
"assets": _ASSET_FILTER_SCHEMA,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
"example": {"scope": "daily", "freshness": "cached", "assets": ["BTC", "ETH"]},
|
|
102
|
+
"output_example": {
|
|
103
|
+
"scope": "daily",
|
|
104
|
+
"freshness": "cached",
|
|
105
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
106
|
+
"age_seconds": 3600,
|
|
107
|
+
"sentiment": {"available": True},
|
|
108
|
+
"macro": {"available": True, "sources": []},
|
|
109
|
+
"etf": {"available": True, "assets": {}},
|
|
110
|
+
"breadth": {"available": True},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"tool_name": "get_context_bundle",
|
|
115
|
+
"description": (
|
|
116
|
+
"Full ContextBundle JSON: portfolio allocation drift vs target, "
|
|
117
|
+
"market, sentiment, macro, regime hints, and delta vs the prior "
|
|
118
|
+
"saved snapshot. Optional assets, target_pct, and band override "
|
|
119
|
+
"server config for drift math."
|
|
120
|
+
),
|
|
121
|
+
"input_schema": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"scope": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"enum": ["daily", "weekly"],
|
|
127
|
+
},
|
|
128
|
+
"freshness": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"enum": ["cached", "live"],
|
|
131
|
+
},
|
|
132
|
+
"assets": _ASSET_FILTER_SCHEMA,
|
|
133
|
+
"target_pct": _TARGET_PCT_SCHEMA,
|
|
134
|
+
"band": _BAND_SCHEMA,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"example": {
|
|
138
|
+
"scope": "daily",
|
|
139
|
+
"freshness": "cached",
|
|
140
|
+
"assets": ["BTC", "ETH"],
|
|
141
|
+
"target_pct": {"BTC": 0.70, "ETH": 0.30, "CASH": 0.0},
|
|
142
|
+
"band": 0.15,
|
|
143
|
+
},
|
|
144
|
+
"output_example": {
|
|
145
|
+
"bundle_id": "daily:2026-05-21T12:00:00+00:00",
|
|
146
|
+
"scope": "daily",
|
|
147
|
+
"assets": ["BTC", "ETH"],
|
|
148
|
+
"portfolio": {"available": True},
|
|
149
|
+
"market": {"available": True},
|
|
150
|
+
"sentiment": {"available": True},
|
|
151
|
+
"macro": {"available": True},
|
|
152
|
+
"regime": {"available": True, "summary": "Portfolio allocation is within the configured drift band."},
|
|
153
|
+
"delta": {"available": True},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"tool_name": "get_rebalance_plan",
|
|
158
|
+
"description": (
|
|
159
|
+
"Compute USD deltas and exchange-style move lines to rebalance a "
|
|
160
|
+
"BTC/ETH/CASH portfolio toward target allocation from current "
|
|
161
|
+
"weights and NAV. Pure math — rebalance plan for btc/eth splits."
|
|
162
|
+
),
|
|
163
|
+
"input_schema": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"allocation_pct": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"description": "Current weights keyed by BTC, ETH, CASH.",
|
|
169
|
+
"properties": {
|
|
170
|
+
"BTC": {"type": "number"},
|
|
171
|
+
"ETH": {"type": "number"},
|
|
172
|
+
"CASH": {"type": "number"},
|
|
173
|
+
},
|
|
174
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
175
|
+
},
|
|
176
|
+
"target_pct": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"description": "Target weights keyed by BTC, ETH, CASH.",
|
|
179
|
+
"properties": {
|
|
180
|
+
"BTC": {"type": "number"},
|
|
181
|
+
"ETH": {"type": "number"},
|
|
182
|
+
"CASH": {"type": "number"},
|
|
183
|
+
},
|
|
184
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
185
|
+
},
|
|
186
|
+
"nav_usd": {
|
|
187
|
+
"type": "number",
|
|
188
|
+
"description": "Portfolio NAV in USD.",
|
|
189
|
+
},
|
|
190
|
+
"exchange": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"enum": ["kraken", "coinbase"],
|
|
193
|
+
"description": (
|
|
194
|
+
"Exchange-specific move wording (default kraken)."
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
"band": _BAND_SCHEMA,
|
|
198
|
+
},
|
|
199
|
+
"required": ["allocation_pct", "target_pct", "nav_usd"],
|
|
200
|
+
},
|
|
201
|
+
"example": {
|
|
202
|
+
"allocation_pct": {"BTC": 0.45, "ETH": 0.45, "CASH": 0.10},
|
|
203
|
+
"target_pct": {"BTC": 0.50, "ETH": 0.40, "CASH": 0.10},
|
|
204
|
+
"nav_usd": 10000,
|
|
205
|
+
"exchange": "kraken",
|
|
206
|
+
"band": 0.15,
|
|
207
|
+
},
|
|
208
|
+
"output_example": {
|
|
209
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
210
|
+
"age_seconds": 0,
|
|
211
|
+
"exchange": "kraken",
|
|
212
|
+
"moves": [],
|
|
213
|
+
"deltas_usd": {"BTC": 500.0, "ETH": -500.0, "CASH": 0.0},
|
|
214
|
+
"band_check": {"outside_band": False, "hint": "within_band"},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"tool_name": "get_portfolio_state",
|
|
219
|
+
"description": (
|
|
220
|
+
"Live portfolio read: NAV, BTC/ETH/CASH allocation, drift vs "
|
|
221
|
+
"target, and band hint. Pass read-only kraken or coinbase credentials "
|
|
222
|
+
"in the request; never stored server-side."
|
|
223
|
+
),
|
|
224
|
+
"input_schema": {
|
|
225
|
+
"type": "object",
|
|
226
|
+
"properties": {
|
|
227
|
+
"exchange": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"enum": ["kraken", "coinbase"],
|
|
230
|
+
"description": "Spot exchange to query.",
|
|
231
|
+
},
|
|
232
|
+
"api_key": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"description": "Read-only API key (CDP key name for Coinbase).",
|
|
235
|
+
},
|
|
236
|
+
"api_secret": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": (
|
|
239
|
+
"Read-only API secret (Kraken base64 secret or Coinbase EC PEM)."
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
"target_pct": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"description": "Optional target weights; defaults to server config.",
|
|
245
|
+
"properties": {
|
|
246
|
+
"BTC": {"type": "number"},
|
|
247
|
+
"ETH": {"type": "number"},
|
|
248
|
+
"CASH": {"type": "number"},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
"band": {
|
|
252
|
+
"type": "number",
|
|
253
|
+
"description": "Drift band width (default from server config).",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
"required": ["exchange", "api_key", "api_secret"],
|
|
257
|
+
},
|
|
258
|
+
"example": {
|
|
259
|
+
"exchange": "kraken",
|
|
260
|
+
"api_key": "YOUR_READ_ONLY_KEY",
|
|
261
|
+
"api_secret": "YOUR_READ_ONLY_SECRET",
|
|
262
|
+
},
|
|
263
|
+
"output_example": {
|
|
264
|
+
"available": True,
|
|
265
|
+
"exchange": "kraken",
|
|
266
|
+
"source": "live",
|
|
267
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
268
|
+
"age_seconds": 0,
|
|
269
|
+
"nav_usd": 10000.0,
|
|
270
|
+
"allocation_pct": {"BTC": 0.70, "ETH": 0.25, "CASH": 0.05},
|
|
271
|
+
"rebalance_hint": "within_band",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"tool_name": "check_allocation_band",
|
|
276
|
+
"description": (
|
|
277
|
+
"Check whether BTC/ETH/CASH allocation is outside a drift band vs "
|
|
278
|
+
"target and return hint (within_band, consider_rebalance, etc.)."
|
|
279
|
+
),
|
|
280
|
+
"input_schema": {
|
|
281
|
+
"type": "object",
|
|
282
|
+
"properties": {
|
|
283
|
+
"allocation_pct": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"properties": {
|
|
286
|
+
"BTC": {"type": "number"},
|
|
287
|
+
"ETH": {"type": "number"},
|
|
288
|
+
"CASH": {"type": "number"},
|
|
289
|
+
},
|
|
290
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
291
|
+
},
|
|
292
|
+
"target_pct": {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"BTC": {"type": "number"},
|
|
296
|
+
"ETH": {"type": "number"},
|
|
297
|
+
"CASH": {"type": "number"},
|
|
298
|
+
},
|
|
299
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
300
|
+
},
|
|
301
|
+
"band": {
|
|
302
|
+
"type": "number",
|
|
303
|
+
"description": "Drift band width (default 0.15 = 15%).",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
"required": ["allocation_pct", "target_pct"],
|
|
307
|
+
},
|
|
308
|
+
"example": {
|
|
309
|
+
"allocation_pct": {"BTC": 0.45, "ETH": 0.45, "CASH": 0.10},
|
|
310
|
+
"target_pct": {"BTC": 0.50, "ETH": 0.40, "CASH": 0.10},
|
|
311
|
+
"band": 0.15,
|
|
312
|
+
},
|
|
313
|
+
"output_example": {
|
|
314
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
315
|
+
"age_seconds": 0,
|
|
316
|
+
"outside_band": False,
|
|
317
|
+
"hint": "within_band",
|
|
318
|
+
"max_drift": 0.05,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"tool_name": "get_context_at",
|
|
323
|
+
"description": (
|
|
324
|
+
"Load a saved ContextBundle snapshot from ingest history by ISO "
|
|
325
|
+
"timestamp (match exact or at_or_before)."
|
|
326
|
+
),
|
|
327
|
+
"input_schema": {
|
|
328
|
+
"type": "object",
|
|
329
|
+
"properties": {
|
|
330
|
+
"as_of": {"type": "string", "description": "ISO timestamp."},
|
|
331
|
+
"scope": {"type": "string", "enum": ["daily", "weekly"]},
|
|
332
|
+
"match": {"type": "string", "enum": ["exact", "at_or_before"]},
|
|
333
|
+
"assets": _ASSET_FILTER_SCHEMA,
|
|
334
|
+
},
|
|
335
|
+
"required": ["as_of"],
|
|
336
|
+
},
|
|
337
|
+
"example": {
|
|
338
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
339
|
+
"scope": "daily",
|
|
340
|
+
"match": "at_or_before",
|
|
341
|
+
},
|
|
342
|
+
"output_example": {
|
|
343
|
+
"scope": "daily",
|
|
344
|
+
"as_of": "2026-05-21T12:00:00+00:00",
|
|
345
|
+
"portfolio": {"available": True},
|
|
346
|
+
"regime": {"available": True},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"tool_name": "get_context_delta",
|
|
351
|
+
"description": (
|
|
352
|
+
"Compare two ContextBundle snapshots and return notable_shifts."
|
|
353
|
+
),
|
|
354
|
+
"input_schema": {
|
|
355
|
+
"type": "object",
|
|
356
|
+
"properties": {
|
|
357
|
+
"prior_as_of": {"type": "string"},
|
|
358
|
+
"scope": {"type": "string", "enum": ["daily", "weekly"]},
|
|
359
|
+
"current_as_of": {"type": "string"},
|
|
360
|
+
"assets": _ASSET_FILTER_SCHEMA,
|
|
361
|
+
},
|
|
362
|
+
"required": ["prior_as_of"],
|
|
363
|
+
},
|
|
364
|
+
"example": {
|
|
365
|
+
"prior_as_of": "2026-05-20T12:00:00+00:00",
|
|
366
|
+
"scope": "daily",
|
|
367
|
+
},
|
|
368
|
+
"output_example": {
|
|
369
|
+
"prior_as_of": "2026-05-20T12:00:00+00:00",
|
|
370
|
+
"current_as_of": "2026-05-21T12:00:00+00:00",
|
|
371
|
+
"notable_shifts": ["F&G 30 → 25 (-5)"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"tool_name": "check_allocation_bands",
|
|
376
|
+
"description": (
|
|
377
|
+
"Evaluate allocation drift against multiple target/band scenarios."
|
|
378
|
+
),
|
|
379
|
+
"input_schema": {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"properties": {
|
|
382
|
+
"allocation_pct": {
|
|
383
|
+
"type": "object",
|
|
384
|
+
"properties": {
|
|
385
|
+
"BTC": {"type": "number"},
|
|
386
|
+
"ETH": {"type": "number"},
|
|
387
|
+
"CASH": {"type": "number"},
|
|
388
|
+
},
|
|
389
|
+
"required": ["BTC", "ETH", "CASH"],
|
|
390
|
+
},
|
|
391
|
+
"scenarios": {
|
|
392
|
+
"type": "array",
|
|
393
|
+
"items": {
|
|
394
|
+
"type": "object",
|
|
395
|
+
"properties": {
|
|
396
|
+
"name": {"type": "string"},
|
|
397
|
+
"target_pct": {
|
|
398
|
+
"type": "object",
|
|
399
|
+
"properties": {
|
|
400
|
+
"BTC": {"type": "number"},
|
|
401
|
+
"ETH": {"type": "number"},
|
|
402
|
+
"CASH": {"type": "number"},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
"band": {"type": "number"},
|
|
406
|
+
},
|
|
407
|
+
"required": ["target_pct"],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
"required": ["allocation_pct", "scenarios"],
|
|
412
|
+
},
|
|
413
|
+
"example": {
|
|
414
|
+
"allocation_pct": {"BTC": 0.65, "ETH": 0.30, "CASH": 0.05},
|
|
415
|
+
"scenarios": [
|
|
416
|
+
{
|
|
417
|
+
"name": "base",
|
|
418
|
+
"target_pct": {"BTC": 0.70, "ETH": 0.30, "CASH": 0.00},
|
|
419
|
+
"band": 0.15,
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
"output_example": {
|
|
424
|
+
"allocation_pct": {"BTC": 0.65, "ETH": 0.30, "CASH": 0.05},
|
|
425
|
+
"scenarios": [{"name": "base", "hint": "within_band"}],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
_TOOL_NAMES = tuple(spec["tool_name"] for spec in _MCP_TOOLS)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def mcp_tool_specs() -> tuple[dict[str, Any], ...]:
|
|
434
|
+
return _MCP_TOOLS
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def smoke_tool_arguments(tool_name: str) -> dict[str, Any]:
|
|
438
|
+
"""Example args for paid smoke / re-index scripts (hosted-safe defaults)."""
|
|
439
|
+
from datetime import datetime, timedelta, timezone
|
|
440
|
+
|
|
441
|
+
for spec in _MCP_TOOLS:
|
|
442
|
+
if spec["tool_name"] != tool_name:
|
|
443
|
+
continue
|
|
444
|
+
args = dict(spec["example"])
|
|
445
|
+
now = datetime.now(timezone.utc)
|
|
446
|
+
if tool_name == "get_context_at":
|
|
447
|
+
args["as_of"] = now.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
|
448
|
+
args["match"] = "at_or_before"
|
|
449
|
+
args.setdefault("scope", "daily")
|
|
450
|
+
elif tool_name == "get_context_delta":
|
|
451
|
+
# Hosted ingest keeps recent hourly snapshots; 2h back finds a prior point.
|
|
452
|
+
prior = now - timedelta(hours=2)
|
|
453
|
+
args["prior_as_of"] = prior.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
|
454
|
+
args.setdefault("scope", "daily")
|
|
455
|
+
return args
|
|
456
|
+
raise KeyError(f"unknown MCP tool: {tool_name}")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def public_mcp_url(*, base_url: str, mcp_path: str) -> str:
|
|
460
|
+
return f"{base_url.rstrip('/')}{mcp_path}"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def resolve_public_base_url() -> str | None:
|
|
464
|
+
for key in ("X402_PUBLIC_URL", "ALLOC_CONTEXT_MCP_PUBLIC_URL"):
|
|
465
|
+
value = os.environ.get(key, "").strip()
|
|
466
|
+
if value:
|
|
467
|
+
return value.rstrip("/")
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def build_mcp_tool_extensions() -> dict[str, dict[str, Any]]:
|
|
472
|
+
"""Per-tool Bazaar MCP extensions keyed by tool name."""
|
|
473
|
+
extensions: dict[str, dict[str, Any]] = {}
|
|
474
|
+
for spec in _MCP_TOOLS:
|
|
475
|
+
extensions[spec["tool_name"]] = declare_mcp_discovery_extension(
|
|
476
|
+
DeclareMcpDiscoveryConfig(
|
|
477
|
+
tool_name=spec["tool_name"],
|
|
478
|
+
description=spec["description"],
|
|
479
|
+
transport="streamable-http",
|
|
480
|
+
input_schema=spec["input_schema"],
|
|
481
|
+
example=spec["example"],
|
|
482
|
+
output=OutputConfig(example=spec["output_example"]),
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
return extensions
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def build_http_route_extensions() -> dict[str, Any]:
|
|
489
|
+
"""Bazaar extension for the paid POST /mcp streamable HTTP endpoint."""
|
|
490
|
+
primary = _MCP_TOOLS[0]
|
|
491
|
+
return declare_discovery_extension(
|
|
492
|
+
input={
|
|
493
|
+
"jsonrpc": "2.0",
|
|
494
|
+
"method": "tools/call",
|
|
495
|
+
"params": {
|
|
496
|
+
"name": primary["tool_name"],
|
|
497
|
+
"arguments": primary["example"],
|
|
498
|
+
},
|
|
499
|
+
"id": 1,
|
|
500
|
+
},
|
|
501
|
+
input_schema={
|
|
502
|
+
"type": "object",
|
|
503
|
+
"properties": {
|
|
504
|
+
"jsonrpc": {"type": "string", "const": "2.0"},
|
|
505
|
+
"method": {
|
|
506
|
+
"type": "string",
|
|
507
|
+
"description": "MCP JSON-RPC method (e.g. tools/call).",
|
|
508
|
+
},
|
|
509
|
+
"params": {
|
|
510
|
+
"type": "object",
|
|
511
|
+
"properties": {
|
|
512
|
+
"name": {
|
|
513
|
+
"type": "string",
|
|
514
|
+
"enum": list(_TOOL_NAMES),
|
|
515
|
+
"description": (
|
|
516
|
+
"AllocContext tool: get_market_context, "
|
|
517
|
+
"get_context_bundle, get_rebalance_plan, "
|
|
518
|
+
"get_portfolio_state, check_allocation_band, "
|
|
519
|
+
"get_context_at, get_context_delta, or "
|
|
520
|
+
"check_allocation_bands."
|
|
521
|
+
),
|
|
522
|
+
},
|
|
523
|
+
"arguments": {
|
|
524
|
+
"type": "object",
|
|
525
|
+
"description": "Tool-specific arguments object.",
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
"required": ["name"],
|
|
529
|
+
},
|
|
530
|
+
"id": {"type": ["integer", "string"]},
|
|
531
|
+
},
|
|
532
|
+
"required": ["jsonrpc", "method", "params"],
|
|
533
|
+
},
|
|
534
|
+
body_type="json",
|
|
535
|
+
output=OutputConfig(
|
|
536
|
+
example={
|
|
537
|
+
"jsonrpc": "2.0",
|
|
538
|
+
"id": 1,
|
|
539
|
+
"result": primary["output_example"],
|
|
540
|
+
}
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def build_llms_txt(
|
|
546
|
+
*,
|
|
547
|
+
public_url: str,
|
|
548
|
+
mcp_path: str,
|
|
549
|
+
accepted_stables: tuple[str, ...] = ("USDC", "EURC"),
|
|
550
|
+
) -> str:
|
|
551
|
+
endpoint = public_mcp_url(base_url=public_url, mcp_path=mcp_path)
|
|
552
|
+
tool_lines = "\n".join(
|
|
553
|
+
f"- `{spec['tool_name']}` — {spec['description']}" for spec in _MCP_TOOLS
|
|
554
|
+
)
|
|
555
|
+
return f"""# {SERVICE_TITLE}
|
|
556
|
+
|
|
557
|
+
{LISTING_DESCRIPTION}
|
|
558
|
+
|
|
559
|
+
## Paid MCP (x402, Base stablecoins)
|
|
560
|
+
|
|
561
|
+
- Endpoint: `{endpoint}`
|
|
562
|
+
- Transport: streamable HTTP (`POST {mcp_path}`)
|
|
563
|
+
- Health: `{public_url.rstrip('/')}/health` (free)
|
|
564
|
+
- Payment: x402 exact scheme on Base; payer picks one of
|
|
565
|
+
**{", ".join(accepted_stables)}** (USD-pegged; bridge to Base first).
|
|
566
|
+
- Pricing: **$0.02** cached context/math; **$0.05** live ingest or live portfolio.
|
|
567
|
+
|
|
568
|
+
## Tools
|
|
569
|
+
|
|
570
|
+
{tool_lines}
|
|
571
|
+
|
|
572
|
+
## Search keywords
|
|
573
|
+
|
|
574
|
+
bitcoin, ethereum, btc, eth, portfolio allocation, allocation drift,
|
|
575
|
+
rebalance plan, rebalance btc eth, drift band, band check, market context,
|
|
576
|
+
fear and greed, sentiment, macro calendar, etf flows, agent tools, mcp, x402
|
|
577
|
+
|
|
578
|
+
## Examples
|
|
579
|
+
|
|
580
|
+
Redacted tool JSON samples (evaluate before paying):
|
|
581
|
+
https://github.com/negillett/alloc-context/blob/main/docs/examples.md
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def build_well_known_x402(
|
|
586
|
+
*,
|
|
587
|
+
public_url: str,
|
|
588
|
+
mcp_path: str,
|
|
589
|
+
pay_to: str,
|
|
590
|
+
price_light: str = "$0.02",
|
|
591
|
+
price_heavy: str = "$0.05",
|
|
592
|
+
network: str = "eip155:84532",
|
|
593
|
+
accepted_stables: tuple[str, ...] = ("USDC", "EURC"),
|
|
594
|
+
) -> dict[str, Any]:
|
|
595
|
+
endpoint = public_mcp_url(base_url=public_url, mcp_path=mcp_path)
|
|
596
|
+
return {
|
|
597
|
+
"name": SERVICE_NAME,
|
|
598
|
+
"title": SERVICE_TITLE,
|
|
599
|
+
"description": LISTING_DESCRIPTION,
|
|
600
|
+
"tags": list(SERVICE_TAGS),
|
|
601
|
+
"resources": [
|
|
602
|
+
{
|
|
603
|
+
"url": endpoint,
|
|
604
|
+
"type": "http",
|
|
605
|
+
"description": LISTING_DESCRIPTION,
|
|
606
|
+
"tools": [
|
|
607
|
+
{
|
|
608
|
+
"name": spec["tool_name"],
|
|
609
|
+
"description": spec["description"],
|
|
610
|
+
"inputSchema": spec["input_schema"],
|
|
611
|
+
}
|
|
612
|
+
for spec in _MCP_TOOLS
|
|
613
|
+
],
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
"payment": {
|
|
617
|
+
"scheme": "exact",
|
|
618
|
+
"payTo": pay_to,
|
|
619
|
+
"pricing": {
|
|
620
|
+
"cached_context_and_math": price_light,
|
|
621
|
+
"live_ingest_or_portfolio": price_heavy,
|
|
622
|
+
"network": network,
|
|
623
|
+
"assets": list(accepted_stables),
|
|
624
|
+
"note": (
|
|
625
|
+
"Payer chooses one listed stable on Base; amounts are "
|
|
626
|
+
"USD-pegged per call tier."
|
|
627
|
+
),
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
}
|