@coinrithm/mcp-trading 0.7.14 → 0.7.15

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.
@@ -54,18 +54,48 @@ export function chatShapeFor(provider, model, baseUrl) {
54
54
  minProbeCompletionTokens: 1024,
55
55
  };
56
56
  }
57
+ // Transport-only restriction: drop the excluded variants from the action oneOf
58
+ // a route decodes against. parseDecision keeps validating the FULL contract, so
59
+ // a route that does not enforce a schema can still propose the action and the
60
+ // validator rejects it. Never returns an empty oneOf.
61
+ export function restrictDecisionSchema(schema, exclude) {
62
+ if (!exclude?.length)
63
+ return schema;
64
+ const properties = schema.properties;
65
+ const actions = properties?.actions;
66
+ const variants = actions?.items?.oneOf;
67
+ if (!properties || !actions || !Array.isArray(variants))
68
+ return schema;
69
+ const kept = variants.filter((variant) => {
70
+ const type = variant
71
+ .properties?.type?.const;
72
+ return !exclude.includes(type);
73
+ });
74
+ if (kept.length === variants.length || kept.length === 0)
75
+ return schema;
76
+ return {
77
+ ...schema,
78
+ properties: {
79
+ ...properties,
80
+ actions: { ...actions, items: { ...actions.items, oneOf: kept } },
81
+ },
82
+ };
83
+ }
57
84
  /** Build the chat-completions body for a route from its capability shape. */
58
85
  export function buildChatBody(shape, args) {
59
86
  const system = shape.systemHint
60
87
  ? `${shape.systemHint}\n\n${args.system}`
61
88
  : args.system;
89
+ const jsonSchema = shape.jsonSchema
90
+ ? restrictDecisionSchema(shape.jsonSchema, args.excludeActionTypes)
91
+ : undefined;
62
92
  return {
63
93
  model: args.model,
64
94
  ...(shape.allowsTemperature
65
95
  ? { temperature: args.temperature ?? 0.2 }
66
96
  : {}),
67
97
  [shape.tokenParam]: args.maxTokens,
68
- ...(shape.jsonSchema && shape.jsonSchemaTransport === "tool_call"
98
+ ...(jsonSchema && shape.jsonSchemaTransport === "tool_call"
69
99
  ? {
70
100
  tools: [
71
101
  {
@@ -73,7 +103,7 @@ export function buildChatBody(shape, args) {
73
103
  function: {
74
104
  name: DECISION_TOOL_NAME,
75
105
  description: "Submit the complete CoinRithm paper-trading decision for this cycle.",
76
- parameters: shape.jsonSchema,
106
+ parameters: jsonSchema,
77
107
  },
78
108
  },
79
109
  ],
@@ -85,12 +115,12 @@ export function buildChatBody(shape, args) {
85
115
  : {}),
86
116
  ...(shape.jsonResponseFormat && shape.jsonSchemaTransport !== "tool_call"
87
117
  ? {
88
- response_format: shape.jsonSchema
118
+ response_format: jsonSchema
89
119
  ? {
90
120
  type: "json_schema",
91
121
  json_schema: {
92
122
  name: "coinrithm_trading_decision",
93
- schema: shape.jsonSchema,
123
+ schema: jsonSchema,
94
124
  },
95
125
  }
96
126
  : { type: "json_object" },
@@ -1,9 +1,11 @@
1
1
  import { AgentSpec, ProviderName } from "./types.js";
2
+ import { type DecisionActionExclusion } from "./providerCapabilities.js";
2
3
  export interface DecideInput {
3
4
  system: string;
4
5
  user: string;
5
6
  maxTokens?: number;
6
7
  timeoutMs?: number;
8
+ excludeActionTypes?: readonly DecisionActionExclusion[];
7
9
  }
8
10
  export interface DecideRouteAttempt {
9
11
  provider: string;
@@ -26,6 +28,7 @@ export interface DecideRouteMeta {
26
28
  export type DecideResult = {
27
29
  ok: true;
28
30
  text: string;
31
+ responseSource?: "tool_call" | "content_fallback" | "content";
29
32
  usage?: {
30
33
  promptTokens: number;
31
34
  completionTokens: number;
@@ -193,7 +193,7 @@ class AnthropicProvider {
193
193
  }
194
194
  : undefined;
195
195
  return text
196
- ? { ok: true, text, usage }
196
+ ? { ok: true, text, usage, responseSource: "content" }
197
197
  : { ok: false, error: "anthropic returned empty content" };
198
198
  });
199
199
  }
@@ -246,6 +246,7 @@ class OpenAiCompatProvider {
246
246
  system: input.system,
247
247
  user: input.user,
248
248
  maxTokens: input.maxTokens ?? 1024,
249
+ excludeActionTypes: input.excludeActionTypes,
249
250
  })),
250
251
  });
251
252
  if (!res.ok) {
@@ -273,7 +274,16 @@ class OpenAiCompatProvider {
273
274
  }
274
275
  : undefined;
275
276
  return text
276
- ? { ok: true, text, usage }
277
+ ? {
278
+ ok: true,
279
+ text,
280
+ usage,
281
+ responseSource: decisionArguments != null
282
+ ? "tool_call"
283
+ : shape.jsonSchemaTransport === "tool_call"
284
+ ? "content_fallback"
285
+ : "content",
286
+ }
277
287
  : { ok: false, error: "provider returned empty content" };
278
288
  });
279
289
  }
@@ -9,10 +9,11 @@ import { usesCapitalSizing, prepareCapitalAction, validateCapitalAction, capital
9
9
  import { decideMechanical } from "./mechanical.js";
10
10
  import { evaluateGate, noteLlmCall, estimateCostUsd } from "./gate.js";
11
11
  import { baseSymbol, scanSetups } from "./setups.js";
12
+ import { futuresEntryPreflight } from "./futuresEligibility.js";
12
13
  import { reconcileObservation } from "./reconcileObservation.js";
13
14
  import { createOpportunityReporter } from "./opportunityReporter.js";
14
- import { observe } from "./observe.js";
15
- import { buildDailyRiskBudget, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
15
+ import { enrichWhaleContext, observe } from "./observe.js";
16
+ import { buildDailyRiskBudget, buildFuturesCapacity, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
16
17
  import { parseDecision } from "./decision.js";
17
18
  import { validateAction, isRiskIncreasingAction, } from "./decisionValidator.js";
18
19
  import { resolvePmRef } from "./resolvePm.js";
@@ -435,6 +436,19 @@ async function runCycleCore(deps, capture) {
435
436
  const decisionId = makeDecisionId(state.cyclesRun);
436
437
  const captureBase = { runId, decisionId, spec, mergedProse, state };
437
438
  capture({ ...captureBase, phase: "before_observation" });
439
+ // These exits happen before both provider invocation and action execution.
440
+ // Record that known absence explicitly; missing metering means unknown to
441
+ // audit consumers. Do not default later failures to zero: a call/write may
442
+ // already have occurred by then.
443
+ const preDecisionMeter = {
444
+ llmCallMade: false,
445
+ decisionType: "skip",
446
+ tokensIn: 0,
447
+ tokensOut: 0,
448
+ estimatedCostUsd: 0,
449
+ writeAttempted: 0,
450
+ writeAccepted: 0,
451
+ };
438
452
  // Kill-switch pre-check: a disabled agent never observes, decides, or acts.
439
453
  const tripped = checkKillSwitch(spec, state);
440
454
  if (tripped) {
@@ -448,6 +462,7 @@ async function runCycleCore(deps, capture) {
448
462
  disabled: true,
449
463
  disabledReason: tripped,
450
464
  live,
465
+ ...preDecisionMeter,
451
466
  };
452
467
  }
453
468
  // Establish durable run identity before the first possible write. A process
@@ -511,6 +526,7 @@ async function runCycleCore(deps, capture) {
511
526
  disabled: true,
512
527
  disabledReason: state.disabledReason,
513
528
  live,
529
+ ...preDecisionMeter,
514
530
  ...observationReceipt,
515
531
  };
516
532
  }
@@ -535,6 +551,7 @@ async function runCycleCore(deps, capture) {
535
551
  disabled: true,
536
552
  disabledReason: state.disabledReason,
537
553
  live,
554
+ ...preDecisionMeter,
538
555
  ...observationReceipt,
539
556
  };
540
557
  }
@@ -546,6 +563,7 @@ async function runCycleCore(deps, capture) {
546
563
  skipReason: obs.skip,
547
564
  planned: [],
548
565
  live,
566
+ ...preDecisionMeter,
549
567
  ...observationReceipt,
550
568
  };
551
569
  }
@@ -624,15 +642,15 @@ async function runCycleCore(deps, capture) {
624
642
  // so it touches NO kill-switch counter; it just records the cheap cycle.
625
643
  const policy = spec.triggerPolicy ?? DEFAULT_TRIGGER_POLICY;
626
644
  const gate = evaluateGate(observation, state, policy, nowMs);
627
- capture({
628
- ...captureBase,
629
- phase: "decision_input",
630
- observation,
631
- observationFingerprint: observationReceipt.observationHash,
632
- preThesisObservationFingerprint,
633
- });
634
645
  const providerName = spec.model?.provider ?? "nvidia";
635
646
  if (!gate.fire) {
647
+ capture({
648
+ ...captureBase,
649
+ phase: "decision_input",
650
+ observation,
651
+ observationFingerprint: observationReceipt.observationHash,
652
+ preThesisObservationFingerprint,
653
+ });
636
654
  saveState(stateFile, state);
637
655
  log(`gate: skip (${gate.reason})`);
638
656
  return {
@@ -651,6 +669,20 @@ async function runCycleCore(deps, capture) {
651
669
  ...observationReceipt,
652
670
  };
653
671
  }
672
+ if (providerName !== "mechanical" &&
673
+ spec.capabilities.includes("whale_context") &&
674
+ spec.venues.includes("pm")) {
675
+ observation.whaleContext = await enrichWhaleContext(client, observation);
676
+ observationReceipt = buildObservationReceipt(observation);
677
+ Object.assign(baseTrace, observationReceipt);
678
+ }
679
+ capture({
680
+ ...captureBase,
681
+ phase: "decision_input",
682
+ observation,
683
+ observationFingerprint: observationReceipt.observationHash,
684
+ preThesisObservationFingerprint,
685
+ });
654
686
  // DECIDE. Two paths share the same downstream validate+act loop:
655
687
  // • mechanical benchmark agents (provider "mechanical") compute the decision
656
688
  // deterministically from the observation — no prompt, no model call, no
@@ -678,19 +710,43 @@ async function runCycleCore(deps, capture) {
678
710
  };
679
711
  }
680
712
  else {
713
+ const futuresCapacity = spec.venues.includes("futures")
714
+ ? buildFuturesCapacity(spec, observation)
715
+ : undefined;
716
+ // One exclusion drives both the textual action menu and transport schema.
717
+ // It is a proposal hint only; parsing and validation stay unchanged.
718
+ const capacitySpent = futuresCapacity !== undefined &&
719
+ (futuresCapacity.slotsLeft === 0 ||
720
+ futuresCapacity.marginHeadroomMusd <= 0);
721
+ const actionAvailability = capacitySpent
722
+ ? { excludeActionTypes: ["futures_open"] }
723
+ : {};
681
724
  const system = buildSystemPrompt(spec, mergedProse, {
682
725
  includeForecast: forecastEnabled,
726
+ ...actionAvailability,
683
727
  });
684
728
  const user = buildUserPrompt(observation, state.journal, {
685
729
  venues: spec.venues,
686
730
  dailyRiskBudget: buildDailyRiskBudget(spec, state),
687
731
  ...(usesCapitalSizing(spec) ? { capitalSizing: spec.capitalSizing } : {}),
732
+ ...(futuresCapacity ? { futuresCapacity } : {}),
733
+ ...actionAvailability,
688
734
  });
689
735
  const tokensInEst = Math.round((system.length + user.length) / 4);
690
736
  // Prompt-size + trigger visibility in the live terminal.
691
737
  log(`prompt ~${tokensInEst} tok ` +
692
738
  `(pm ${observation.pmMarkets.length}, trades ${observation.newClosedTrades.length}, watch ${observation.watch.length}, setups ${observation.setups.length}, triggers ${gate.codes.join("|") || "none"})`);
693
- const res = await provider.decide({ system, user });
739
+ // With either futures cap spent, a schema-enforcing route is not offered
740
+ // futures_open at all. Other routes can still propose it; the validator
741
+ // rejects it there exactly as before.
742
+ if (capacitySpent) {
743
+ log(`futures capacity spent (slotsLeft ${futuresCapacity.slotsLeft}, marginHeadroomMusd ${futuresCapacity.marginHeadroomMusd}): futures_open withheld from schema-enforcing routes`);
744
+ }
745
+ const res = await provider.decide({
746
+ system,
747
+ user,
748
+ ...actionAvailability,
749
+ });
694
750
  const route = res.route;
695
751
  const actualCallMade = route
696
752
  ? route.attempts.some((attempt) => attempt.outcome !== "deferred")
@@ -841,6 +897,22 @@ async function runCycleCore(deps, capture) {
841
897
  state.consecutivePermanentModelErrors = 0;
842
898
  delete state.permanentModelErrorRoute;
843
899
  const parsed = parseDecision(res.text);
900
+ if (capacitySpent) {
901
+ // Persist only route provenance and a parsed action-presence bit in the
902
+ // existing cycle log. Unknown adapters and malformed decisions must not
903
+ // be miscounted as tool compliance; raw output stays unretained.
904
+ const responseSource = res.responseSource === "tool_call" ||
905
+ res.responseSource === "content_fallback" ||
906
+ res.responseSource === "content"
907
+ ? res.responseSource
908
+ : "unknown";
909
+ log(`capacity-response ${JSON.stringify({
910
+ responseSource,
911
+ withheldActionProposed: parsed.ok
912
+ ? parsed.decision.actions.some((action) => action.type === "futures_open")
913
+ : null,
914
+ })}`);
915
+ }
844
916
  if (!parsed.ok) {
845
917
  state.consecutiveModelFailures += 1;
846
918
  saveState(stateFile, state);
@@ -966,7 +1038,7 @@ async function runCycleCore(deps, capture) {
966
1038
  log(`reject pm_open: duplicate_intent (hold PM ${pm.slug})`);
967
1039
  continue;
968
1040
  }
969
- // Independent forecast submission (HOUSE_AGENT_FORECAST_ENABLED, default ON).
1041
+ // Market-aware forecast submission (HOUSE_AGENT_FORECAST_ENABLED, default ON).
970
1042
  // Attach the model's OWN probability the backed side wins — clamped to [1,99],
971
1043
  // OMITTED when absent/unparseable (a bad forecast never blocks the trade), and
972
1044
  // NEVER defaulted to the market price. Flag OFF strips any forecast so the open
@@ -1058,6 +1130,23 @@ async function runCycleCore(deps, capture) {
1058
1130
  }
1059
1131
  }
1060
1132
  }
1133
+ // Supported-reference preflight (backend-v2 #106, J-74): skip a NEW futures
1134
+ // open on a coin the observation EXPLICITLY reports has no supported (or only
1135
+ // a stale) perpetual reference, before spending a quote the server gate would
1136
+ // refuse. Unknown eligibility, adds/management on a held coin, spot and PM
1137
+ // pass; the server gate stays authoritative at quote/open time.
1138
+ const preflight = futuresEntryPreflight(action, observation);
1139
+ if (preflight) {
1140
+ planned.push({
1141
+ action,
1142
+ accepted: false,
1143
+ code: preflight.code,
1144
+ ...(capitalSizing ? { capitalSizing } : {}),
1145
+ reason: preflight.reason,
1146
+ });
1147
+ log(`reject ${action.type}: ${preflight.code}`);
1148
+ continue;
1149
+ }
1061
1150
  const quote = await fetchQuote(client, action, observation, baseTrace, {
1062
1151
  pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct,
1063
1152
  });
@@ -50,37 +50,37 @@ const PRESETS = {
50
50
  modelFail: 5,
51
51
  },
52
52
  };
53
- const THESIS_BODY = `# Momentum Futures — strategy
54
-
55
- You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
56
- Everything here is simulated; it is not financial advice and never touches real
57
- money. Edit this prose freely (any language) — it is your agent's borders.
58
-
59
- ## Each cycle
60
-
61
- 1. Ground yourself: read your portfolio and open positions first. Never assume
62
- balances or what is already open.
63
- 2. Scan the watchlist. A candidate is a coin whose short and medium momentum
64
- agree (both up, or both down) and is not already an open position.
65
- 3. Pick the strongest candidate and commit when the read is clear — even a
66
- moderate-confidence one — sized small with a stop. Skip only when the signals
67
- contradict or the data is stale; a quiet tape where your edge is still real
68
- is an act, not a skip.
69
- 4. Quote before you open. Read the liquidation price and confirm it is sane. If
70
- the quote is not eligible, relay the reason and stop.
71
- 5. Open small and protected: enter in the trend direction and set a stop-loss at
72
- open. Place the take-profit a touch wider than the stop.
73
- 6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
74
- fired while you were not looking, and react to what actually happened.
75
-
76
- The hard caps (leverage, margin, watchlist) live in the config blocks above and
53
+ const THESIS_BODY = `# Momentum Futures — strategy
54
+
55
+ You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
56
+ Everything here is simulated; it is not financial advice and never touches real
57
+ money. Edit this prose freely (any language) — it is your agent's borders.
58
+
59
+ ## Each cycle
60
+
61
+ 1. Ground yourself: read your portfolio and open positions first. Never assume
62
+ balances or what is already open.
63
+ 2. Scan the watchlist. A candidate is a coin whose short and medium momentum
64
+ agree (both up, or both down) and is not already an open position.
65
+ 3. Pick the strongest candidate and commit when the read is clear — even a
66
+ moderate-confidence one — sized small with a stop. Skip only when the signals
67
+ contradict or the data is stale; a quiet tape where your edge is still real
68
+ is an act, not a skip.
69
+ 4. Quote before you open. Read the liquidation price and confirm it is sane. If
70
+ the quote is not eligible, relay the reason and stop.
71
+ 5. Open small and protected: enter in the trend direction and set a stop-loss at
72
+ open. Place the take-profit a touch wider than the stop.
73
+ 6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
74
+ fired while you were not looking, and react to what actually happened.
75
+
76
+ The hard caps (leverage, margin, watchlist) live in the config blocks above and
77
77
  are enforced by the runner — change them there, not in this prose.`;
78
- const PERSONA_STUB = `# Persona
79
-
80
- Decisive and in character. Acts on a clear read — even a moderate-confidence one
81
- — sized small and protected with a stop, and skips only when the read is
82
- contradictory or the data is stale. States its reasoning plainly in its own
83
- voice, and never frames paper results as real-money advice.
78
+ const PERSONA_STUB = `# Persona
79
+
80
+ Decisive and in character. Acts on a clear read — even a moderate-confidence one
81
+ — sized small and protected with a stop, and skips only when the read is
82
+ contradictory or the data is stale. States its reasoning plainly in its own
83
+ voice, and never frames paper results as real-money advice.
84
84
  `;
85
85
  export function buildAgentObject(name, preset) {
86
86
  const p = PRESETS[preset];
@@ -1,3 +1,4 @@
1
+ import type { FuturesEntryEligibility } from "./futuresEligibility.js";
1
2
  import { IndicatorSet } from "./indicators.js";
2
3
  import type { DecisionInputRecord } from "./decisionReceipt.js";
3
4
  export declare const SPEC_VERSION = "coinrithm.agent.v1";
@@ -97,7 +98,7 @@ export interface ObjectiveConfig {
97
98
  secondary: string[];
98
99
  horizon?: string;
99
100
  }
100
- export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan"];
101
+ export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan", "whale_context"];
101
102
  export type Capability = (typeof ALLOWED_CAPABILITIES)[number];
102
103
  export interface AgentSpec {
103
104
  name: string;
@@ -158,12 +159,16 @@ export interface WatchEntry {
158
159
  change24h?: number;
159
160
  change7d?: number;
160
161
  sentimentBullishPct?: number;
162
+ sentimentTotalVotes?: number;
163
+ sentimentDayUtc?: string;
164
+ sentimentUpdatedAt?: string;
161
165
  freshness?: Freshness;
162
166
  indicators?: IndicatorSet;
163
167
  indicatorContext?: IndicatorContext;
164
168
  discovered?: boolean;
165
169
  slug?: string;
166
170
  fundamentals?: CoinFundamentals;
171
+ futuresEntryEligibility?: FuturesEntryEligibility;
167
172
  }
168
173
  export interface CoinFundamentals {
169
174
  categories?: string[];
@@ -247,6 +252,42 @@ export interface PmMarket {
247
252
  liquidityUsd?: number;
248
253
  quality?: PmQuality;
249
254
  decisionSupport?: PmDecisionSupport;
255
+ consensus?: PmConsensus | null;
256
+ rules?: PmSettlementRule | null;
257
+ outcomeRule?: PmOutcomeRule;
258
+ }
259
+ export interface PmCalibration {
260
+ settled: number;
261
+ brierAgent: number | null;
262
+ brierMarket: number | null;
263
+ meanForecastPct: number | null;
264
+ winRatePct: number | null;
265
+ bands: Array<{
266
+ fromPct: number;
267
+ toPct: number;
268
+ n: number;
269
+ meanForecastPct: number;
270
+ winRatePct: number;
271
+ }>;
272
+ }
273
+ export interface PmSettlementRule {
274
+ published: boolean;
275
+ text?: string;
276
+ sources?: string[];
277
+ scope?: "per_outcome";
278
+ }
279
+ export type PmOutcomeRule = {
280
+ primary: string;
281
+ secondary?: string;
282
+ } | {
283
+ unknown: "source_rules_unavailable" | "market_not_found" | "rule_missing" | "conflicting_duplicates" | "unreadable";
284
+ };
285
+ export interface PmConsensus {
286
+ prob: number;
287
+ venues: number;
288
+ spreadPts: number;
289
+ kind: "binary" | "leader";
290
+ outcome: string | null;
250
291
  }
251
292
  export interface PmQuality {
252
293
  decisionEligible?: boolean;
@@ -282,6 +323,78 @@ export interface NewsItem {
282
323
  publishedAt?: string;
283
324
  coins?: string[];
284
325
  }
326
+ export type WhaleContextStatus = "available" | "partial" | "unavailable";
327
+ export type WhaleContextFailure = "timeout" | "network_error" | "http_error" | "invalid_payload";
328
+ export interface WhaleTradeContext {
329
+ source: string;
330
+ eventSlug: string;
331
+ eventTitle?: string;
332
+ marketQuestion?: string;
333
+ side: string;
334
+ outcome?: string;
335
+ usdValue?: number;
336
+ price?: number;
337
+ sourceMarketRef?: string;
338
+ valueBasis?: string;
339
+ evidenceType?: string;
340
+ evidenceRef?: string;
341
+ nativeValue?: number;
342
+ nativeCurrency?: string;
343
+ availability?: "live" | "delayed" | "unavailable";
344
+ tradedAt?: string;
345
+ observedAt?: string;
346
+ latencySeconds?: number;
347
+ walletAddress?: string;
348
+ }
349
+ export interface WhaleWalletFillContext {
350
+ eventSlug?: string;
351
+ eventTitle?: string;
352
+ marketQuestion?: string;
353
+ side?: string;
354
+ outcome?: string;
355
+ usdValue?: number;
356
+ price?: number;
357
+ evidenceType?: string;
358
+ tradedAt?: string;
359
+ sourceMarketRef?: string;
360
+ }
361
+ export interface WhaleWalletContext {
362
+ source: string;
363
+ address: string;
364
+ asOf?: string;
365
+ summary30d?: {
366
+ basis?: string;
367
+ windowStart?: string;
368
+ tradeCount?: number;
369
+ notionalUsd?: number;
370
+ buyNotionalUsd?: number;
371
+ sellNotionalUsd?: number;
372
+ };
373
+ rollup?: {
374
+ available?: boolean;
375
+ coveredDays?: number;
376
+ from?: string;
377
+ to?: string;
378
+ computedAt?: string;
379
+ };
380
+ daily: Array<{
381
+ day?: string;
382
+ tradeCount?: number;
383
+ buyCount?: number;
384
+ sellCount?: number;
385
+ notionalUsd?: number;
386
+ }>;
387
+ recentFills: WhaleWalletFillContext[];
388
+ }
389
+ export interface WhaleContext {
390
+ status: WhaleContextStatus;
391
+ fetchedAt: string;
392
+ coverage: "relevant_events" | "no_relevant_events";
393
+ trades: WhaleTradeContext[];
394
+ wallets: WhaleWalletContext[];
395
+ omitted: number;
396
+ reason?: WhaleContextFailure;
397
+ }
285
398
  export interface Observation {
286
399
  asOf: string;
287
400
  scopes: string[];
@@ -293,16 +406,19 @@ export interface Observation {
293
406
  pmPositions: PmPosition[];
294
407
  pmResolutions: PmResolution[];
295
408
  pmMarkets: PmMarket[];
409
+ pmCalibration?: PmCalibration;
296
410
  watch: WatchEntry[];
297
411
  setups: SetupSignal[];
298
412
  marketMood?: {
299
413
  fearGreed: number;
300
414
  label: string;
415
+ fetchedAt?: string;
301
416
  };
302
417
  syncCursor: string | null;
303
418
  newClosedTrades: Array<Record<string, unknown>>;
304
419
  polledBeforeWrite: boolean;
305
420
  news?: NewsItem[];
421
+ whaleContext?: WhaleContext;
306
422
  universeMovers?: Array<{
307
423
  symbol: string;
308
424
  name?: string;
@@ -50,6 +50,9 @@ 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
+ // `whale_context` reads bounded public prediction-market whale context only
54
+ // after the deterministic gate fires, and only when PM is in the venue scope.
55
+ // It never widens the candidate set or acts as a trading signal by itself.
53
56
  // `universe_scan` (2026-08-18, direct user request): each cycle the runner
54
57
  // pulls the top 24h movers across CoinRithm's tracked coin universe, resolves
55
58
  // the top few into FULL watch entries (price, sentiment, indicators when that
@@ -64,6 +67,7 @@ export const ALLOWED_CAPABILITIES = [
64
67
  "indicators",
65
68
  "news",
66
69
  "universe_scan",
70
+ "whale_context",
67
71
  ];
68
72
  export const ok = () => ({ valid: true });
69
73
  export const fail = (code, reason) => ({
package/dist/client.d.ts CHANGED
@@ -39,6 +39,7 @@ export type ProvenanceReport = {
39
39
  sourceCapturedAt?: string;
40
40
  };
41
41
  };
42
+ export declare const API_KEY_MINT_URL = "https://www.coinrithm.com/en/settings/api-keys";
42
43
  export declare class CoinRithmClient {
43
44
  private readonly defaultApiKey?;
44
45
  private readonly baseUrl;
@@ -113,6 +114,12 @@ export declare class CoinRithmClient {
113
114
  range?: "1H" | "1D" | "1W" | "1M" | "3M";
114
115
  fiat?: string;
115
116
  }, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
117
+ getNews(query: {
118
+ coins: string;
119
+ limit?: number;
120
+ hours?: number;
121
+ minImportance?: number;
122
+ }, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
116
123
  discoverPmMarkets(query?: {
117
124
  q?: string;
118
125
  source?: "all" | "kalshi" | "polymarket";
@@ -145,8 +152,8 @@ export declare class CoinRithmClient {
145
152
  page?: number;
146
153
  pageSize?: number;
147
154
  window?: "7d" | "30d" | "all";
148
- }, apiKey?: string): Promise<ApiResult>;
149
- getArenaAgent(handle: string, apiKey?: string): Promise<ApiResult>;
155
+ }): Promise<ApiResult>;
156
+ getArenaAgent(handle: string): Promise<ApiResult>;
150
157
  listOpenOrders(query?: {
151
158
  coinId?: string;
152
159
  limit?: number;