@coinrithm/mcp-trading 0.7.4 → 0.7.6

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.
@@ -14,9 +14,10 @@ import { validateAction } from "./decisionValidator.js";
14
14
  import { resolvePmRef } from "./resolvePm.js";
15
15
  import { fetchQuote, executeAction } from "./act.js";
16
16
  import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
17
- import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
17
+ import { rollDay, checkKillSwitch, accrueRealized, saveState, isPermanentModelError, isAuthFailureSkip, PERMANENT_MODEL_ERROR_THRESHOLD, AUTH_FAILURE_THRESHOLD, } 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,14 +424,46 @@ 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;
432
+ // Permanent-failure classification: a revoked/invalid CoinRithm key
433
+ // answers 401 deterministically — after the threshold, disable with the
434
+ // machine-readable 'key_invalid' prefix the scheduler's self-heal exempts
435
+ // (the old path revived such agents into ~1,500 guaranteed-dead
436
+ // cycles/day). A transient rotation blip stays under the threshold.
437
+ if (isAuthFailureSkip(obs.skip)) {
438
+ state.consecutiveAuthFailures = (state.consecutiveAuthFailures ?? 0) + 1;
439
+ if (state.consecutiveAuthFailures >= AUTH_FAILURE_THRESHOLD) {
440
+ state.disabled = true;
441
+ state.disabledReason = `key_invalid: CoinRithm key rejected (HTTP 401) on ${state.consecutiveAuthFailures} consecutive cycles`;
442
+ saveState(stateFile, state);
443
+ log(`disabled: ${state.disabledReason}`);
444
+ return {
445
+ decision: "skip",
446
+ skipReason: obs.skip,
447
+ planned: [],
448
+ disabled: true,
449
+ disabledReason: state.disabledReason,
450
+ live,
451
+ ...observationReceipt,
452
+ };
453
+ }
454
+ }
426
455
  saveState(stateFile, state);
427
456
  log(`skip: ${obs.skip}`);
428
- return { decision: "skip", skipReason: obs.skip, planned: [], live };
457
+ return {
458
+ decision: "skip",
459
+ skipReason: obs.skip,
460
+ planned: [],
461
+ live,
462
+ ...observationReceipt,
463
+ };
429
464
  }
465
+ // A full observation implies /me succeeded — the auth-failure streak is over.
466
+ state.consecutiveAuthFailures = 0;
430
467
  // GATE (slice 2): only SPEND an LLM call when a deterministic trigger fires — a
431
468
  // flagged entry setup or an open position to manage. No trigger => a cheap
432
469
  // heartbeat (zero tokens). A heartbeat is neither a model reject nor a failure,
@@ -451,6 +488,7 @@ export async function runCycle(deps) {
451
488
  estimatedCostUsd: 0,
452
489
  writeAttempted: 0,
453
490
  writeAccepted: 0,
491
+ ...observationReceipt,
454
492
  };
455
493
  }
456
494
  // DECIDE. Two paths share the same downstream validate+act loop:
@@ -507,6 +545,40 @@ export async function runCycle(deps) {
507
545
  };
508
546
  if (!res.ok) {
509
547
  state.consecutiveModelFailures += 1;
548
+ // Permanent-failure classification: a 404/model_not_found is a
549
+ // DECOMMISSIONED or misconfigured model that will fail every cycle
550
+ // forever (live-measured: 93% of one agent's cycles for days, revived
551
+ // 7x in 3h). Three consecutive occurrences rules out a routing fluke;
552
+ // then disable with the 'model_unavailable' prefix the scheduler's
553
+ // self-heal exempts. Transient errors reset the permanent streak.
554
+ if (isPermanentModelError(res.error)) {
555
+ state.consecutivePermanentModelErrors =
556
+ (state.consecutivePermanentModelErrors ?? 0) + 1;
557
+ if (state.consecutivePermanentModelErrors >=
558
+ PERMANENT_MODEL_ERROR_THRESHOLD) {
559
+ state.disabled = true;
560
+ state.disabledReason = `model_unavailable: ${res.error.slice(0, 160)}`;
561
+ saveState(stateFile, state);
562
+ log(`disabled: ${state.disabledReason}`);
563
+ return {
564
+ decision: "skip",
565
+ skipReason: `model error: ${res.error}`,
566
+ planned: [],
567
+ modelFailed: true,
568
+ disabled: true,
569
+ disabledReason: state.disabledReason,
570
+ live,
571
+ ...meter,
572
+ decisionType: "model_error",
573
+ writeAttempted: 0,
574
+ writeAccepted: 0,
575
+ ...observationReceipt,
576
+ };
577
+ }
578
+ }
579
+ else {
580
+ state.consecutivePermanentModelErrors = 0;
581
+ }
510
582
  saveState(stateFile, state);
511
583
  log(`model error: ${res.error}`);
512
584
  return {
@@ -519,6 +591,7 @@ export async function runCycle(deps) {
519
591
  decisionType: "model_error",
520
592
  writeAttempted: 0,
521
593
  writeAccepted: 0,
594
+ ...observationReceipt,
522
595
  };
523
596
  }
524
597
  const parsed = parseDecision(res.text);
@@ -539,9 +612,11 @@ export async function runCycle(deps) {
539
612
  decisionType: "model_error",
540
613
  writeAttempted: 0,
541
614
  writeAccepted: 0,
615
+ ...observationReceipt,
542
616
  };
543
617
  }
544
618
  state.consecutiveModelFailures = 0;
619
+ state.consecutivePermanentModelErrors = 0;
545
620
  decision = parsed.decision;
546
621
  }
547
622
  // Reasoning captured for the Arena terminal (keystone transparency): the
@@ -578,6 +653,7 @@ export async function runCycle(deps) {
578
653
  writeAttempted: decision.actions.length,
579
654
  writeAccepted: 0,
580
655
  ...(postedOpportunity ? { opportunity: postedOpportunity } : {}),
656
+ ...observationReceipt,
581
657
  };
582
658
  }
583
659
  // VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
@@ -778,13 +854,16 @@ export async function runCycle(deps) {
778
854
  const seq = state.intentSeq[intentKey] ?? 0;
779
855
  const idem = `${runId}:${intentKey}:${seq}`;
780
856
  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));
857
+ const trace = {
858
+ ...makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence,
859
+ // The trade's "why" on the Arena live floor. Prefer the model's per-action
860
+ // summary; else the decision rationale, but kept HONEST about this action's
861
+ // market (a multi-action decision's rationale can be about a DIFFERENT market
862
+ // than a secondary trade — see rationaleForAction). Sanitized short reasoning
863
+ // only — never raw chain-of-thought.
864
+ rationaleForAction(action, decision.rationale, meta.rationaleSummary, decision.actions.length)),
865
+ ...observationReceipt,
866
+ };
788
867
  const r = await executeAction(client, action, observation, trace, idem, provenance);
789
868
  planned.push({
790
869
  action,
@@ -868,6 +947,7 @@ export async function runCycle(deps) {
868
947
  writeAttempted: decision.actions.length,
869
948
  writeAccepted: planned.filter((p) => p.accepted).length,
870
949
  ...(postedOpportunity ? { opportunity: postedOpportunity } : {}),
950
+ ...observationReceipt,
871
951
  };
872
952
  }
873
953
  export async function runLoop(deps, opts = {}) {
@@ -4,4 +4,9 @@ export declare function loadState(file: string | undefined, runId: string): RunS
4
4
  export declare function saveState(file: string | undefined, state: RunState): void;
5
5
  export declare function rollDay(state: RunState): RunState;
6
6
  export declare function accrueRealized(state: RunState, closedTrades: Record<string, unknown>[]): void;
7
+ export declare const PERMANENT_MODEL_ERROR_RE: RegExp;
8
+ export declare const PERMANENT_MODEL_ERROR_THRESHOLD = 3;
9
+ export declare const AUTH_FAILURE_THRESHOLD = 10;
10
+ export declare const isPermanentModelError: (error: string) => boolean;
11
+ export declare const isAuthFailureSkip: (skipReason: string) => boolean;
7
12
  export declare function checkKillSwitch(spec: AgentSpec, state: RunState): string | null;
@@ -84,6 +84,26 @@ export function accrueRealized(state, closedTrades) {
84
84
  // floored at this many consecutive failures regardless of an agent's own (lower)
85
85
  // setting. The scheduler additionally auto-revives any model-failure disable.
86
86
  const MODEL_FAILURE_FLOOR = 10;
87
+ // ── Permanent-failure classification (2026-08-19) ───────────────────────────
88
+ // The generic kill-switch treats every failure as transient — correct for
89
+ // timeouts/blips, catastrophic for DETERMINISTIC failures. Live-measured: one
90
+ // agent spent 93% of 782 cycles/24h on a Groq 404 (model decommissioned),
91
+ // revived 7 times in 3h by the self-heal; four others burned ~1,500 cycles/day
92
+ // on a revoked CoinRithm key (HTTP 401). These classifiers give such failures
93
+ // a fast, NON-revivable disable with a machine-readable reason prefix the
94
+ // scheduler's self-heal exempts ('model_unavailable' / 'key_invalid').
95
+ //
96
+ // Permanent model errors are deterministic, so the threshold is small — 3
97
+ // consecutive occurrences rules out a one-off routing fluke without burning a
98
+ // day. Auth failures get 10: a key rotation/propagation blip should not kill
99
+ // an agent, but nothing recovers from an actually-revoked key.
100
+ export const PERMANENT_MODEL_ERROR_RE = /model_not_found|model[_ ]decommissioned|has been decommissioned|\b404\b|does not exist or you do not have access/i;
101
+ export const PERMANENT_MODEL_ERROR_THRESHOLD = 3;
102
+ export const AUTH_FAILURE_THRESHOLD = 10;
103
+ export const isPermanentModelError = (error) => PERMANENT_MODEL_ERROR_RE.test(error);
104
+ // The observe phase folds a rejected key into its required-reads skip reason
105
+ // as "... (HTTP 401)".
106
+ export const isAuthFailureSkip = (skipReason) => /HTTP 401/.test(skipReason);
87
107
  // Returns a disable reason if any kill-switch condition is tripped, else null.
88
108
  export function checkKillSwitch(spec, state) {
89
109
  const ks = spec.killSwitch;
@@ -25,8 +25,20 @@ const ALLOWED_KEYS = {
25
25
  "capabilities",
26
26
  "include",
27
27
  "watchlist",
28
+ // Load-bearing since OKF v2 (skill.ts builds the full TriggerPolicy from
29
+ // it) but was missing here, so any bundle actually SETTING it got an
30
+ // unknown_key lint — the knob existed and was unreachable (audit rank 10).
31
+ "triggerPolicy",
28
32
  ],
29
33
  trigger: ["cadence", "timezone", "events"],
34
+ triggerPolicy: [
35
+ "mode",
36
+ "skipLlmWhenNoTrigger",
37
+ "alwaysManageOpenPositions",
38
+ "maxLlmCallsPerHour",
39
+ "debounceMinutes",
40
+ "pmEvalCooldownMinutes",
41
+ ],
30
42
  model: ["provider", "name", "baseUrl"],
31
43
  risk: [
32
44
  "maxLeverage",
@@ -107,6 +119,7 @@ export function strictLint(raw) {
107
119
  lintKeys("$root", raw, issues);
108
120
  for (const block of [
109
121
  "trigger",
122
+ "triggerPolicy",
110
123
  "model",
111
124
  "risk",
112
125
  "limits",
@@ -91,6 +91,12 @@ export function buildAgentObject(name, preset) {
91
91
  trigger: { cadence: p.cadence, timezone: "UTC" },
92
92
  model: { provider: "anthropic", name: "claude-sonnet-4-6" },
93
93
  venues: ["futures"],
94
+ // Without `indicators` the event_driven gate has no setups to fire on and
95
+ // a fresh flat agent heartbeats forever with ZERO model calls (audit
96
+ // 2026-08-19: every scaffold was born dormant). Optional extras a user
97
+ // can add: "universe_scan" (top-movers discovery beyond the watchlist)
98
+ // and "news" (catalyst context for its coins).
99
+ capabilities: ["indicators"],
94
100
  risk: {
95
101
  maxLeverage: p.leverage,
96
102
  perTradeMarginMusd: p.margin,
@@ -61,7 +61,7 @@ export interface ObjectiveConfig {
61
61
  secondary: string[];
62
62
  horizon?: string;
63
63
  }
64
- export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news"];
64
+ export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan"];
65
65
  export type Capability = (typeof ALLOWED_CAPABILITIES)[number];
66
66
  export interface AgentSpec {
67
67
  name: string;
@@ -106,6 +106,7 @@ export interface WatchEntry {
106
106
  sentimentBullishPct?: number;
107
107
  freshness?: Freshness;
108
108
  indicators?: IndicatorSet;
109
+ discovered?: boolean;
109
110
  }
110
111
  export interface OpenPosition {
111
112
  venue: Venue;
@@ -197,6 +198,12 @@ export interface Observation {
197
198
  newClosedTrades: Array<Record<string, unknown>>;
198
199
  polledBeforeWrite: boolean;
199
200
  news?: NewsItem[];
201
+ universeMovers?: Array<{
202
+ symbol: string;
203
+ name?: string;
204
+ change24hPct?: number;
205
+ priceUsd?: number;
206
+ }>;
200
207
  }
201
208
  export type ProposedAction = {
202
209
  type: "futures_open";
@@ -290,6 +297,8 @@ export interface RunState {
290
297
  llmCallTimestamps?: number[];
291
298
  lastLlmCallAt?: number;
292
299
  lastTriggerFingerprint?: string;
300
+ consecutivePermanentModelErrors?: number;
301
+ consecutiveAuthFailures?: number;
293
302
  journal?: Array<{
294
303
  at: string;
295
304
  did: string;
@@ -301,6 +310,8 @@ export interface AgentTrace {
301
310
  strategyLabel?: string;
302
311
  confidence?: number;
303
312
  rationaleSummary?: string;
313
+ observationHash?: string;
314
+ indicatorVersion?: string;
304
315
  }
305
316
  export interface ApiResult {
306
317
  ok: boolean;
@@ -330,6 +341,8 @@ export interface CycleResult {
330
341
  disabled?: boolean;
331
342
  disabledReason?: string;
332
343
  live: boolean;
344
+ observationHash?: string;
345
+ indicatorVersion?: string;
333
346
  triggerCodes?: string[];
334
347
  llmCallMade?: boolean;
335
348
  tokensIn?: number;
@@ -50,10 +50,20 @@ export const OBJECTIVE_PRIMARIES = [
50
50
  // slice. `websearch` = external lookups (an injection surface + a cost — it can
51
51
  // inform reasoning but NEVER widen a cap, since caps live in the runner);
52
52
  // `indicators` = runner-computed RSI/MACD/etc. fed into the observation.
53
+ // `universe_scan` (2026-08-18, direct user request): each cycle the runner
54
+ // pulls the top 24h movers across CoinRithm's tracked coin universe, resolves
55
+ // the top few into FULL watch entries (price, sentiment, indicators when that
56
+ // capability is also on) and appends them to the observation marked
57
+ // `discovered: true`. Downstream is unchanged by design: a discovered entry
58
+ // passes through the exact same risk gates as a watchlist symbol (blocklist
59
+ // still wins, caps/SL rules unchanged) — the capability widens the CANDIDATE
60
+ // SET for one cycle, never any cap. Off by default; without it the universe
61
+ // is invisible and only manual watchlist pairs are analyzed.
53
62
  export const ALLOWED_CAPABILITIES = [
54
63
  "websearch",
55
64
  "indicators",
56
65
  "news",
66
+ "universe_scan",
57
67
  ];
58
68
  export const ok = () => ({ valid: true });
59
69
  export const fail = (code, reason) => ({
@@ -5,7 +5,7 @@ export declare const COINRITHM_API: {
5
5
  readonly kind: "coinrithm-agent-api";
6
6
  readonly baseUrl: "https://api.coinrithm.com";
7
7
  readonly mcpUrl: "https://mcp.coinrithm.com/mcp";
8
- readonly openapiVersion: "1.6.0";
8
+ readonly openapiVersion: "1.7.0";
9
9
  readonly mcpPackage: "@coinrithm/mcp-trading";
10
10
  readonly mcpVersion: string;
11
11
  };
@@ -20,7 +20,7 @@ export const COINRITHM_API = {
20
20
  mcpUrl: "https://mcp.coinrithm.com/mcp",
21
21
  // The API CONTRACT version (openapi.yaml info.version). Versioned independently
22
22
  // from the npm package below — hand-bump this when the OpenAPI contract changes.
23
- openapiVersion: "1.6.0",
23
+ openapiVersion: "1.7.0",
24
24
  mcpPackage: "@coinrithm/mcp-trading",
25
25
  mcpVersion: PACKAGE_VERSION,
26
26
  };
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;
@@ -79,6 +83,7 @@ export declare class CoinRithmClient {
79
83
  }): Promise<ApiResult>;
80
84
  getPublicPmCanonicalDetail(key: string): Promise<ApiResult>;
81
85
  getPublicPmVolumeHistory(): Promise<ApiResult>;
86
+ getPublicCryptoMovers(direction: "gainers" | "losers", limit?: number): Promise<ApiResult>;
82
87
  whoami(apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
83
88
  getPortfolio(query?: {
84
89
  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
  }
@@ -230,6 +236,16 @@ export class CoinRithmClient {
230
236
  getPublicPmVolumeHistory() {
231
237
  return this.publicRequest("/api/prediction-markets/volume-history");
232
238
  }
239
+ // ---- public crypto data (no key required) ----
240
+ // Top 24h movers across the tracked coin universe (user feature request,
241
+ // 2026-08-18: agents previously could only analyze manually-added pairs).
242
+ // Backend caps limit at 100; rows are {ucid, symbol, name, slug, change24h,
243
+ // currentPrice} ordered by 24h change.
244
+ getPublicCryptoMovers(direction, limit) {
245
+ return this.publicRequest(direction === "losers"
246
+ ? "/api/coins/top-losers"
247
+ : "/api/coins/top-gainers", { limit });
248
+ }
233
249
  // Every method takes an optional trailing `apiKey` (the per-request key for
234
250
  // the multi-user HTTP path). When omitted, the constructor key (stdio) is used.
235
251
  // ---- reads (scope: read) ----
package/dist/tools.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare function compactPublicPmEvents(data: unknown): unknown;
14
14
  */
15
15
  export declare function compactPublicPmEvent(data: unknown): unknown;
16
16
  export declare function compactPublicPmWhales(data: unknown, limit: number): unknown;
17
+ export declare function compactPublicCryptoMovers(data: unknown): unknown;
17
18
  /**
18
19
  * Keep cross-venue disagreement clusters small enough for an agent context
19
20
  * window: each event is reduced to eventSummary (drops descriptions, images,
package/dist/tools.js CHANGED
@@ -492,6 +492,34 @@ export function compactPublicPmWhales(data, limit) {
492
492
  : data.trades;
493
493
  return { ...data, trades };
494
494
  }
495
+ // Crypto movers rows arrive as {ucid, symbol, name, slug, change24h,
496
+ // currentPrice} with the numerics as STRINGS (backend SQL fn passthrough).
497
+ // Coerce so a brain never string-compares "9.5" > "12"; drop the internal
498
+ // ucid. A row that fails coercion passes through untouched — honesty over
499
+ // polish, same rule as every other compactor here.
500
+ export function compactPublicCryptoMovers(data) {
501
+ if (!Array.isArray(data))
502
+ return data;
503
+ return data.map((row) => {
504
+ if (!isJsonRecord(row))
505
+ return row;
506
+ const change = Number(row.change24h);
507
+ const price = Number(row.currentPrice);
508
+ return {
509
+ // coinId IS the ucid the rest of the tool surface takes (get_candles,
510
+ // get_market_context, the futures quote/open path). Carrying it through
511
+ // is not cosmetic: without it the caller has to re-derive the coin from
512
+ // the SYMBOL via resolve_symbol, and symbols collide across listings —
513
+ // the round-trip can land on a different coin than the one that moved.
514
+ coinId: row.ucid,
515
+ symbol: row.symbol,
516
+ name: row.name,
517
+ slug: row.slug,
518
+ change24hPct: Number.isFinite(change) ? change : row.change24h,
519
+ priceUsd: Number.isFinite(price) ? price : row.currentPrice,
520
+ };
521
+ });
522
+ }
495
523
  const MATCH_PAIR_FIELDS = [
496
524
  "matchId",
497
525
  "confidence",
@@ -1426,8 +1454,8 @@ export function registerTools(server, client) {
1426
1454
  title: "Cross-venue prediction-market statistics",
1427
1455
  description: "Free public cross-venue prediction-market statistics: total/open/" +
1428
1456
  "closed market counts, total volume, 24h volume, and liquidity " +
1429
- "aggregated across all 11 venues (Polymarket, Kalshi, Rothera, " +
1430
- "Limitless, Smarkets, Manifold, Metaculus, PredictIt, Futuur, Myriad, ForecastEx), plus market " +
1457
+ "aggregated across all 12 venues (Polymarket, Kalshi, Rothera, " +
1458
+ "Limitless, Smarkets, Manifold, Metaculus, PredictIt, Futuur, Myriad, ForecastEx, Gemini), plus market " +
1431
1459
  "highlights in a compact discovery shape. Use pm_data_event for full " +
1432
1460
  "event evidence. Freshness is SOURCE-AWARE — each venue ingests " +
1433
1461
  "independently; per-venue health (freshness tier, lag, stale reason) " +
@@ -1445,11 +1473,40 @@ export function registerTools(server, client) {
1445
1473
  outputSchema: API_RESULT_OUTPUT_SCHEMA,
1446
1474
  annotations: readOnlyAnnotations("Cross-venue prediction-market statistics"),
1447
1475
  }, async ({ fiat }) => present(mapSuccessfulBody(await client.getPublicPmOverview({ fiat }), compactPublicPmOverview)));
1476
+ server.registerTool("pm_data_sources", {
1477
+ title: "Prediction-market venue methodology and coverage",
1478
+ description: "Free public methodology and comparable coverage for every CoinRithm " +
1479
+ "prediction-market venue: source kind, supported metrics, market counts, " +
1480
+ "explicit 24h/cumulative volume bases, currency basis, comparability, " +
1481
+ "and as-of timestamps. Use this before comparing venue totals so a " +
1482
+ "completed-day figure is never described as rolling 24h and play-money " +
1483
+ "points are never described as USD. No API key required.",
1484
+ inputSchema: {
1485
+ fiat: z
1486
+ .string()
1487
+ .optional()
1488
+ .describe("Fiat currency code for monetary figures (default usd)."),
1489
+ },
1490
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
1491
+ annotations: readOnlyAnnotations("Prediction-market venue methodology and coverage"),
1492
+ }, async ({ fiat }) => present(await client.getPublicPmSources({ fiat })));
1493
+ server.registerTool("pm_data_sources_health", {
1494
+ title: "Prediction-market venue freshness and health",
1495
+ description: "Free public per-venue ingest health across all CoinRithm sources: " +
1496
+ "freshness tier, observed lag, stale/degraded reason, coverage counts, " +
1497
+ "and current health timestamps. Check this before using a quote or " +
1498
+ "claiming cross-venue coverage; a venue being in the catalogue does " +
1499
+ "not by itself prove its hot prices meet the live freshness target. " +
1500
+ "No API key required.",
1501
+ inputSchema: {},
1502
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
1503
+ annotations: readOnlyAnnotations("Prediction-market venue freshness and health"),
1504
+ }, async () => present(await client.getPublicPmSourcesHealth()));
1448
1505
  server.registerTool("pm_data_events", {
1449
1506
  title: "Search prediction markets across all venues",
1450
- description: "Free public search over prediction-market events across ALL 11 " +
1507
+ description: "Free public search over prediction-market events across ALL 12 " +
1451
1508
  "venues (Polymarket, Kalshi, Rothera, Limitless, Smarkets, " +
1452
- "Manifold, Metaculus, PredictIt, Futuur, Myriad, ForecastEx) — broader than discover_pm_markets, which is " +
1509
+ "Manifold, Metaculus, PredictIt, Futuur, Myriad, ForecastEx, Gemini) — broader than discover_pm_markets, which is " +
1453
1510
  "scoped to the paper-tradeable venues. Returns titles, probabilities, " +
1454
1511
  "volume/liquidity, status, and source per event, plus " +
1455
1512
  "the five highest-probability outcomes and the full outcome count. " +
@@ -1468,7 +1525,7 @@ export function registerTools(server, client) {
1468
1525
  .string()
1469
1526
  .optional()
1470
1527
  .describe("Optional venue filter: polymarket, kalshi, rothera, limitless, " +
1471
- "smarkets, manifold, metaculus, predictit, futuur, myriad, or forecastex."),
1528
+ "smarkets, manifold, metaculus, predictit, futuur, myriad, forecastex, or gemini."),
1472
1529
  status: z
1473
1530
  .string()
1474
1531
  .optional()
@@ -1525,7 +1582,7 @@ export function registerTools(server, client) {
1525
1582
  source: z
1526
1583
  .string()
1527
1584
  .describe("Venue slug: polymarket, kalshi, rothera, limitless, smarkets, " +
1528
- "manifold, metaculus, predictit, futuur, myriad, or forecastex."),
1585
+ "manifold, metaculus, predictit, futuur, myriad, forecastex, or gemini."),
1529
1586
  slug: z.string().describe("Event slug on that venue."),
1530
1587
  fiat: z
1531
1588
  .string()
@@ -1728,4 +1785,32 @@ export function registerTools(server, client) {
1728
1785
  outputSchema: API_RESULT_OUTPUT_SCHEMA,
1729
1786
  annotations: readOnlyAnnotations("Global prediction-market volume trend"),
1730
1787
  }, async () => present(await client.getPublicPmVolumeHistory()));
1788
+ server.registerTool("get_crypto_movers", {
1789
+ title: "Top 24h crypto movers (universe scan)",
1790
+ description: "Free public scan of CoinRithm's tracked crypto universe for the " +
1791
+ "biggest 24h price moves — top gainers or top losers, ordered by " +
1792
+ "24h change percent. Use this to DISCOVER candidates beyond your " +
1793
+ "watchlist (abnormal rapid moves), then deep-analyze each candidate " +
1794
+ "with get_candles (OHLC + indicators) and get_market_context " +
1795
+ "(sentiment, news) before any trade decision. Rows carry coinId, " +
1796
+ "symbol, name, slug, change24hPct and priceUsd; data refreshes on the " +
1797
+ "~60s core price tick. Pass the row's coinId straight to get_candles " +
1798
+ "/ get_market_context — do NOT re-resolve it from the symbol, since " +
1799
+ "symbols collide across listings. No API key required.",
1800
+ inputSchema: {
1801
+ direction: z
1802
+ .enum(["gainers", "losers"])
1803
+ .optional()
1804
+ .describe("Scan direction (default gainers)."),
1805
+ limit: z
1806
+ .number()
1807
+ .int()
1808
+ .min(1)
1809
+ .max(100)
1810
+ .optional()
1811
+ .describe("Rows to return, 1-100 (default 20)."),
1812
+ },
1813
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
1814
+ annotations: readOnlyAnnotations("Top 24h crypto movers"),
1815
+ }, async ({ direction, limit }) => present(mapSuccessfulBody(await client.getPublicCryptoMovers(direction ?? "gainers", limit ?? 20), compactPublicCryptoMovers)));
1731
1816
  }