@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.
- package/CHANGELOG.md +259 -171
- package/README.md +290 -277
- package/dist/agent/cli.js +22 -1
- package/dist/agent/client.d.ts +1 -0
- package/dist/agent/client.js +11 -0
- package/dist/agent/decision.d.ts +12 -0
- package/dist/agent/decision.js +10 -0
- package/dist/agent/indicators.d.ts +1 -0
- package/dist/agent/indicators.js +3 -0
- package/dist/agent/manifest.d.ts +1 -0
- package/dist/agent/manifest.js +3 -0
- package/dist/agent/observationReceipt.d.ts +6 -0
- package/dist/agent/observationReceipt.js +10 -0
- package/dist/agent/observe.js +100 -6
- package/dist/agent/prompt.js +19 -1
- package/dist/agent/resolve.d.ts +12 -0
- package/dist/agent/resolve.js +63 -3
- package/dist/agent/runner.js +89 -9
- package/dist/agent/state.d.ts +5 -0
- package/dist/agent/state.js +20 -0
- package/dist/agent/strictLint.js +13 -0
- package/dist/agent/templates.js +6 -0
- package/dist/agent/types.d.ts +14 -1
- package/dist/agent/types.js +10 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +91 -6
- package/package.json +86 -86
package/dist/agent/runner.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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 = {}) {
|
package/dist/agent/state.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/state.js
CHANGED
|
@@ -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;
|
package/dist/agent/strictLint.js
CHANGED
|
@@ -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",
|
package/dist/agent/templates.js
CHANGED
|
@@ -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,
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/types.js
CHANGED
|
@@ -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) => ({
|
package/dist/agent/version.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
readonly openapiVersion: "1.7.0";
|
|
9
9
|
readonly mcpPackage: "@coinrithm/mcp-trading";
|
|
10
10
|
readonly mcpVersion: string;
|
|
11
11
|
};
|
package/dist/agent/version.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|