@coinrithm/mcp-trading 0.7.3 → 0.7.5

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.
@@ -0,0 +1,10 @@
1
+ // A compact, privacy-safe receipt for the exact structured observation used by
2
+ // one decision. We persist the digest, never the full prompt or model output.
3
+ import { INDICATOR_VERSION } from "./indicators.js";
4
+ import { sha256, stableStringify } from "./util.js";
5
+ export function buildObservationReceipt(observation) {
6
+ return {
7
+ observationHash: sha256(stableStringify(observation)),
8
+ indicatorVersion: INDICATOR_VERSION,
9
+ };
10
+ }
@@ -4,4 +4,8 @@ export interface ObserveOutput {
4
4
  observation: Observation;
5
5
  skip?: string;
6
6
  }
7
+ export declare function isCalibrationChurnMarket(market: {
8
+ slug?: string;
9
+ title?: string;
10
+ }): boolean;
7
11
  export declare function observe(client: CoinRithmClient, spec: AgentSpec, state: RunState, trace?: AgentTrace): Promise<ObserveOutput>;
@@ -28,6 +28,16 @@ const PM_COIN_NAMES = {
28
28
  UNI: "Uniswap",
29
29
  SUI: "Sui",
30
30
  };
31
+ // Repeated micro-contracts are useful for execution smoke tests but are a poor
32
+ // calibration universe: outcomes overlap heavily, resolve too quickly to admit
33
+ // meaningful independent research, and drown the public scorecard in Bitcoin
34
+ // coin flips. Non-mechanical calibration agents receive a deeper discovery
35
+ // page with these rows removed. Mechanical baselines intentionally keep the
36
+ // unmodified universe so their reference contract remains reproducible.
37
+ const PM_CALIBRATION_CHURN_RE = /(updown|up-or-down|-5-?min|-5m-|-15m|15m(?:-|$)|(?:5|15)\s+min(?:ute)?s?|-1h-|hourly|-daily-|\bdaily\b|what-price-will[^\n]*(?:today|tomorrow)|-above-on-|-price-on-|this[ -]week|of[ -]the[ -]week|-weekly-)/i;
38
+ export function isCalibrationChurnMarket(market) {
39
+ return PM_CALIBRATION_CHURN_RE.test(`${market.slug ?? ""} ${market.title ?? ""}`);
40
+ }
31
41
  // Fetch candles for one coin and reduce them to a compact indicator bundle.
32
42
  // Tolerant by design: any failure (HTTP error, malformed/sparse candles) returns
33
43
  // null so the cycle proceeds with price-only context rather than skipping.
@@ -289,6 +299,9 @@ export async function observe(client, spec, state, trace) {
289
299
  let pmResolutions = [];
290
300
  let pmMarkets = [];
291
301
  if (wantPm) {
302
+ const curatedCalibrationBoard = spec.objective?.primary === "calibration" &&
303
+ spec.model?.provider !== "mechanical";
304
+ const primaryDiscoveryLimit = curatedCalibrationBoard ? 30 : 12;
292
305
  // Bias PM discovery toward CRYPTO markets the agent has a price view on — the
293
306
  // only PM edge a price agent reliably has (probed 2026-06-24: the default board
294
307
  // is World Cup / elections / F1, which an agent has no edge on). The discover
@@ -299,7 +312,7 @@ export async function observe(client, spec, state, trace) {
299
312
  const pmQuery = PM_COIN_NAMES[topCoin] ?? spec.risk.watchlist[0] ?? "Bitcoin";
300
313
  const [pmPosR, pmDiscFirst] = await Promise.all([
301
314
  client.pmPositions(undefined, trace),
302
- client.discoverPmMarkets({ q: pmQuery, limit: 12 }, trace),
315
+ client.discoverPmMarkets({ q: pmQuery, limit: primaryDiscoveryLimit }, trace),
303
316
  ]);
304
317
  let pmDiscR = pmDiscFirst;
305
318
  const firstCount = pmDiscR.ok
@@ -378,6 +391,9 @@ export async function observe(client, spec, state, trace) {
378
391
  // quoteable id NESTED at outcomes[].externalMarketId — expandPmMarkets turns
379
392
  // that into one row per quoteable outcome (eligible + not-held filtered).
380
393
  let mergedRows = expandPmMarkets(pmDiscR.data, heldPmKeys);
394
+ if (curatedCalibrationBoard) {
395
+ mergedRows = mergedRows.filter((market) => !isCalibrationChurnMarket(market));
396
+ }
381
397
  // ── Crypto-targeted secondary discover (pm_ref hallucination fix) ────────
382
398
  // The prompt tells the model its SHARPEST PM edge is the crypto price view it
383
399
  // JUST formed — but that is only actionable if the board actually LISTS a
@@ -409,10 +425,13 @@ export async function observe(client, spec, state, trace) {
409
425
  // rows that actually reference the targeted coin (a fuzzy backend match
410
426
  // can't dilute the board with off-topic events).
411
427
  const primaryEventKeys = new Set(mergedRows.map((m) => `${m.source}|${m.slug}`));
412
- const secRows = expandPmMarkets(secR.data, heldPmKeys)
428
+ let secRows = expandPmMarkets(secR.data, heldPmKeys)
413
429
  .filter((m) => titleMentionsCoin(m.title, topAnalyzed))
414
- .filter((m) => !primaryEventKeys.has(`${m.source}|${m.slug}`))
415
- .slice(0, 4);
430
+ .filter((m) => !primaryEventKeys.has(`${m.source}|${m.slug}`));
431
+ if (curatedCalibrationBoard) {
432
+ secRows = secRows.filter((market) => !isCalibrationChurnMarket(market));
433
+ }
434
+ secRows = secRows.slice(0, 4);
416
435
  // Reserve slots for the targeted rows so the 12-cap can't slice off the
417
436
  // very markets the secondary fetch exists to surface. Primary rows keep
418
437
  // priority; the targeted rows are appended.
@@ -17,6 +17,7 @@ import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
17
17
  import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
18
18
  import { asObj, asNum, asStr } from "./extract.js";
19
19
  import { parseCadenceMs, sleep } from "./util.js";
20
+ import { buildObservationReceipt } from "./observationReceipt.js";
20
21
  // Independent-forecast kill-switch. Default ON: the fleet elicits + submits its
21
22
  // OWN forecastProbability on PM opens. Set HOUSE_AGENT_FORECAST_ENABLED to
22
23
  // "false"/"0"/"no"/"off" to ship pm/open requests WITHOUT the field, byte-identical
@@ -371,6 +372,10 @@ export async function runCycle(deps) {
371
372
  // OBSERVE
372
373
  const obs = await observe(client, spec, state, baseTrace);
373
374
  const observation = obs.observation;
375
+ const observationReceipt = buildObservationReceipt(observation);
376
+ // Reads build the observation, so its hash cannot exist before they finish.
377
+ // From this point every durable write carries the exact decision-input receipt.
378
+ Object.assign(baseTrace, observationReceipt);
374
379
  accrueRealized(state, observation.newClosedTrades);
375
380
  state.cursor = observation.syncCursor;
376
381
  for (const t of observation.newClosedTrades) {
@@ -419,13 +424,20 @@ export async function runCycle(deps) {
419
424
  disabled: true,
420
425
  disabledReason: state.disabledReason,
421
426
  live,
427
+ ...observationReceipt,
422
428
  };
423
429
  }
424
430
  if (obs.skip) {
425
431
  state.consecutiveRejectCycles += 1;
426
432
  saveState(stateFile, state);
427
433
  log(`skip: ${obs.skip}`);
428
- return { decision: "skip", skipReason: obs.skip, planned: [], live };
434
+ return {
435
+ decision: "skip",
436
+ skipReason: obs.skip,
437
+ planned: [],
438
+ live,
439
+ ...observationReceipt,
440
+ };
429
441
  }
430
442
  // GATE (slice 2): only SPEND an LLM call when a deterministic trigger fires — a
431
443
  // flagged entry setup or an open position to manage. No trigger => a cheap
@@ -451,6 +463,7 @@ export async function runCycle(deps) {
451
463
  estimatedCostUsd: 0,
452
464
  writeAttempted: 0,
453
465
  writeAccepted: 0,
466
+ ...observationReceipt,
454
467
  };
455
468
  }
456
469
  // DECIDE. Two paths share the same downstream validate+act loop:
@@ -519,6 +532,7 @@ export async function runCycle(deps) {
519
532
  decisionType: "model_error",
520
533
  writeAttempted: 0,
521
534
  writeAccepted: 0,
535
+ ...observationReceipt,
522
536
  };
523
537
  }
524
538
  const parsed = parseDecision(res.text);
@@ -539,6 +553,7 @@ export async function runCycle(deps) {
539
553
  decisionType: "model_error",
540
554
  writeAttempted: 0,
541
555
  writeAccepted: 0,
556
+ ...observationReceipt,
542
557
  };
543
558
  }
544
559
  state.consecutiveModelFailures = 0;
@@ -578,6 +593,7 @@ export async function runCycle(deps) {
578
593
  writeAttempted: decision.actions.length,
579
594
  writeAccepted: 0,
580
595
  ...(postedOpportunity ? { opportunity: postedOpportunity } : {}),
596
+ ...observationReceipt,
581
597
  };
582
598
  }
583
599
  // VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
@@ -778,13 +794,16 @@ export async function runCycle(deps) {
778
794
  const seq = state.intentSeq[intentKey] ?? 0;
779
795
  const idem = `${runId}:${intentKey}:${seq}`;
780
796
  const meta = action;
781
- const trace = makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence,
782
- // The trade's "why" on the Arena live floor. Prefer the model's per-action
783
- // summary; else the decision rationale, but kept HONEST about this action's
784
- // market (a multi-action decision's rationale can be about a DIFFERENT market
785
- // than a secondary trade — see rationaleForAction). Sanitized short reasoning
786
- // only — never raw chain-of-thought.
787
- rationaleForAction(action, decision.rationale, meta.rationaleSummary, decision.actions.length));
797
+ const trace = {
798
+ ...makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence,
799
+ // The trade's "why" on the Arena live floor. Prefer the model's per-action
800
+ // summary; else the decision rationale, but kept HONEST about this action's
801
+ // market (a multi-action decision's rationale can be about a DIFFERENT market
802
+ // than a secondary trade — see rationaleForAction). Sanitized short reasoning
803
+ // only — never raw chain-of-thought.
804
+ rationaleForAction(action, decision.rationale, meta.rationaleSummary, decision.actions.length)),
805
+ ...observationReceipt,
806
+ };
788
807
  const r = await executeAction(client, action, observation, trace, idem, provenance);
789
808
  planned.push({
790
809
  action,
@@ -868,6 +887,7 @@ export async function runCycle(deps) {
868
887
  writeAttempted: decision.actions.length,
869
888
  writeAccepted: planned.filter((p) => p.accepted).length,
870
889
  ...(postedOpportunity ? { opportunity: postedOpportunity } : {}),
890
+ ...observationReceipt,
871
891
  };
872
892
  }
873
893
  export async function runLoop(deps, opts = {}) {
@@ -301,6 +301,8 @@ export interface AgentTrace {
301
301
  strategyLabel?: string;
302
302
  confidence?: number;
303
303
  rationaleSummary?: string;
304
+ observationHash?: string;
305
+ indicatorVersion?: string;
304
306
  }
305
307
  export interface ApiResult {
306
308
  ok: boolean;
@@ -330,6 +332,8 @@ export interface CycleResult {
330
332
  disabled?: boolean;
331
333
  disabledReason?: string;
332
334
  live: boolean;
335
+ observationHash?: string;
336
+ indicatorVersion?: string;
333
337
  triggerCodes?: string[];
334
338
  llmCallMade?: boolean;
335
339
  tokensIn?: number;
package/dist/client.d.ts CHANGED
@@ -48,6 +48,10 @@ export declare class CoinRithmClient {
48
48
  getPublicPmOverview(query?: {
49
49
  fiat?: string;
50
50
  }): Promise<ApiResult>;
51
+ getPublicPmSources(query?: {
52
+ fiat?: string;
53
+ }): Promise<ApiResult>;
54
+ getPublicPmSourcesHealth(): Promise<ApiResult>;
51
55
  listPublicPmEvents(query?: {
52
56
  q?: string;
53
57
  source?: string;
@@ -61,6 +65,24 @@ export declare class CoinRithmClient {
61
65
  fiat?: string;
62
66
  }): Promise<ApiResult>;
63
67
  getPublicPmWhales(): Promise<ApiResult>;
68
+ getPublicPmMatches(query?: {
69
+ limit?: number;
70
+ offset?: number;
71
+ sort?: string;
72
+ minDivergence?: number;
73
+ sourceKind?: string;
74
+ status?: string;
75
+ maxSnapshotAgeMinutes?: number;
76
+ requirePriced?: boolean;
77
+ fiat?: string;
78
+ }): Promise<ApiResult>;
79
+ getPublicPmCalibration(): Promise<ApiResult>;
80
+ getPublicPmCanonicalList(query?: {
81
+ limit?: number;
82
+ cursor?: number;
83
+ }): Promise<ApiResult>;
84
+ getPublicPmCanonicalDetail(key: string): Promise<ApiResult>;
85
+ getPublicPmVolumeHistory(): Promise<ApiResult>;
64
86
  whoami(apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
65
87
  getPortfolio(query?: {
66
88
  fiat?: string;
package/dist/client.js CHANGED
@@ -203,6 +203,12 @@ export class CoinRithmClient {
203
203
  getPublicPmOverview(query) {
204
204
  return this.publicRequest("/api/prediction-markets/overview", query);
205
205
  }
206
+ getPublicPmSources(query) {
207
+ return this.publicRequest("/api/prediction-markets/sources", query);
208
+ }
209
+ getPublicPmSourcesHealth() {
210
+ return this.publicRequest("/api/prediction-markets/sources/health");
211
+ }
206
212
  listPublicPmEvents(query) {
207
213
  return this.publicRequest("/api/prediction-markets/events", query);
208
214
  }
@@ -212,6 +218,24 @@ export class CoinRithmClient {
212
218
  getPublicPmWhales() {
213
219
  return this.publicRequest("/api/prediction-markets/whales");
214
220
  }
221
+ // Cross-venue disagreement clusters (approved event matches, graph-clustered).
222
+ getPublicPmMatches(query) {
223
+ return this.publicRequest("/api/prediction-markets/matches/public", query);
224
+ }
225
+ // Per-venue forecast-accuracy calibration (ECE + reliability curve). No query params.
226
+ getPublicPmCalibration() {
227
+ return this.publicRequest("/api/prediction-markets/calibration");
228
+ }
229
+ getPublicPmCanonicalList(query) {
230
+ return this.publicRequest("/api/prediction-markets/canonical", query);
231
+ }
232
+ getPublicPmCanonicalDetail(key) {
233
+ return this.publicRequest(`/api/prediction-markets/canonical/${encodeURIComponent(key)}`);
234
+ }
235
+ // Global daily volume trend (real-money venues only). No query params.
236
+ getPublicPmVolumeHistory() {
237
+ return this.publicRequest("/api/prediction-markets/volume-history");
238
+ }
215
239
  // Every method takes an optional trailing `apiKey` (the per-request key for
216
240
  // the multi-user HTTP path). When omitted, the constructor key (stdio) is used.
217
241
  // ---- reads (scope: read) ----
package/dist/http.js CHANGED
@@ -46,7 +46,9 @@ async function main() {
46
46
  const app = express();
47
47
  app.use(express.json());
48
48
  // Lightweight, unauthenticated liveness probe (handy for Coolify/uptime checks).
49
- app.get("/healthz", (_req, res) => {
49
+ // Keep `/healthz` as the deployment contract and expose `/health` as a
50
+ // compatibility alias for agents and generic uptime monitors.
51
+ app.get(["/health", "/healthz"], (_req, res) => {
50
52
  res.json({
51
53
  ok: true,
52
54
  service: "coinrithm-mcp",
package/dist/tools.d.ts CHANGED
@@ -1,4 +1,26 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CoinRithmClient } from "./client.js";
3
3
  export declare const PAPER_NOTE: string;
4
+ /**
5
+ * Keep keyless discovery calls small enough for an agent context window.
6
+ * Full event evidence remains available from pm_data_event.
7
+ */
8
+ export declare function compactPublicPmOverview(data: unknown): unknown;
9
+ export declare function compactPublicPmEvents(data: unknown): unknown;
10
+ /**
11
+ * Default event detail for agents: enough provenance and comparison evidence
12
+ * to reason safely without recursively spending an entire context window.
13
+ * Callers can explicitly request detail=full for the untouched API record.
14
+ */
15
+ export declare function compactPublicPmEvent(data: unknown): unknown;
16
+ export declare function compactPublicPmWhales(data: unknown, limit: number): unknown;
17
+ /**
18
+ * Keep cross-venue disagreement clusters small enough for an agent context
19
+ * window: each event is reduced to eventSummary (drops descriptions, images,
20
+ * sparklines) and each pairwise comparison keeps only its top-5 highest-delta
21
+ * shared outcomes (compactComparison) — the same bounding pm_data_event
22
+ * applies to crossSourceMatches. Verified live: a 5-cluster page drops from
23
+ * ~466KB to ~48KB.
24
+ */
25
+ export declare function compactPublicPmDisagreements(data: unknown): unknown;
4
26
  export declare function registerTools(server: McpServer, client: CoinRithmClient): void;