@coinrithm/mcp-trading 0.7.2 → 0.7.3

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +9 -7
  3. package/dist/agent/act.d.ts +2 -2
  4. package/dist/agent/act.js +24 -3
  5. package/dist/agent/cli.js +59 -18
  6. package/dist/agent/client.d.ts +33 -0
  7. package/dist/agent/client.js +34 -7
  8. package/dist/agent/decision.d.ts +3 -0
  9. package/dist/agent/decision.js +26 -3
  10. package/dist/agent/decisionValidator.js +2 -1
  11. package/dist/agent/deploymentOverlay.js +25 -5
  12. package/dist/agent/engine.d.ts +2 -1
  13. package/dist/agent/engine.js +4 -1
  14. package/dist/agent/extract.js +3 -1
  15. package/dist/agent/gate.js +25 -5
  16. package/dist/agent/index.js +0 -1
  17. package/dist/agent/indicators.js +4 -2
  18. package/dist/agent/manifest.js +1 -1
  19. package/dist/agent/mechanical.d.ts +36 -0
  20. package/dist/agent/mechanical.js +286 -0
  21. package/dist/agent/observe.js +120 -52
  22. package/dist/agent/prompt.d.ts +3 -1
  23. package/dist/agent/prompt.js +17 -6
  24. package/dist/agent/providers.js +39 -4
  25. package/dist/agent/resolve.js +23 -6
  26. package/dist/agent/resolvePm.js +14 -3
  27. package/dist/agent/runEvidence.js +6 -2
  28. package/dist/agent/runner.d.ts +8 -2
  29. package/dist/agent/runner.js +363 -59
  30. package/dist/agent/scorecard.js +12 -4
  31. package/dist/agent/setups.js +57 -9
  32. package/dist/agent/skill.js +1 -1
  33. package/dist/agent/state.js +9 -4
  34. package/dist/agent/types.d.ts +17 -2
  35. package/dist/agent/types.js +2 -1
  36. package/dist/agent/util.js +11 -4
  37. package/dist/agent/version.d.ts +1 -1
  38. package/dist/agent/version.js +1 -1
  39. package/dist/client.d.ts +33 -0
  40. package/dist/client.js +12 -3
  41. package/dist/executionPolicy.d.ts +2 -0
  42. package/dist/executionPolicy.js +21 -0
  43. package/dist/http.js +10 -2
  44. package/dist/tools.d.ts +1 -0
  45. package/dist/tools.js +214 -29
  46. package/package.json +9 -1
package/CHANGELOG.md CHANGED
@@ -3,7 +3,52 @@
3
3
  All notable changes to `@coinrithm/mcp-trading` are documented here. The package
4
4
  ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (the
5
5
  self-host agent runner) — versioned together. The CoinRithm **API contract** is
6
- versioned separately (see `openapi.yaml` `info.version`, currently `1.5.0`).
6
+ versioned separately (see `openapi.yaml` `info.version`, currently `1.6.0`).
7
+
8
+ ## 0.7.3
9
+
10
+ Quality-engine surfaces + independent forecasts. Additive; no breaking change.
11
+
12
+ - **Quality verdicts in tool responses.** `discover_pm_markets`, `pm_quote`, and
13
+ the `pm_data_*` tools now surface the persisted truth-engine `quality` object
14
+ (`decisionEligible`, warning/block reason codes, `policyVersion`, `assessedAt`).
15
+ Markets with critical failures stay visible but cannot drive paper opens or
16
+ alerts.
17
+ - **`openBlocked` preview on `pm_quote`.** Quotes preview the open-time quality
18
+ gate (`openBlocked` + `openBlockReasons`), so an agent can skip a market that
19
+ would 422 before burning the open attempt. The self-host runner
20
+ (`coinrithm-agent`) does this skip automatically.
21
+ - **Independent forecasts in the runner.** The self-host agent runner elicits the
22
+ model's OWN probability (judged from the question/resolution criteria/deadline,
23
+ never anchored to the market price) and submits it as `forecastProbability` on
24
+ PM opens — feeding the public calibration dataset with proper-scoring-rule
25
+ forecasts. Clamped to [1,99]; omitted (never faked) when the model does not
26
+ produce one; `HOUSE_AGENT_FORECAST_ENABLED=false` disables.
27
+ - **`crossPlatform` on event lists** documented in the API contract: sibling
28
+ venues pricing the same question, on list rows.
29
+ - **ForecastEx venue truth.** Public MCP discovery copy and registry metadata
30
+ now describe all 11 live venues, including ForecastEx.
31
+ - **Contract synchronization.** Runner templates and example bundles pin the
32
+ served OpenAPI 1.6.0 contract; canonical scorecard paths are unambiguous.
33
+
34
+ ## 0.7.2
35
+
36
+ Docs-truth + privacy release. No tool behavior change, no API-surface change.
37
+
38
+ - **Ten venues in the public listing.** `pm_data_*` tool copy, the README, and
39
+ `server.json` now name all ten venues (adds Futuur and Myriad). npm `0.7.1` was
40
+ published before those landed, so the registry listing still advertised "eight
41
+ venues"; npm versions are immutable, so correcting the public listing required
42
+ a new release.
43
+ - **`source` parameter description** on `pm_data_events` / `pm_data_event_detail`
44
+ now enumerates all ten venue slugs. Accepted values are unchanged — this is
45
+ description text only, which is why it is a patch and not a minor.
46
+ - **Privacy.** Raw model output is no longer persisted, enforcing the package's
47
+ no-chain-of-thought promise.
48
+ - **New tripwire.** `server.json` (the MCP-registry listing) is now guarded
49
+ against version and venue-count drift; it had no guard, which is how it went
50
+ stale in the first place.
51
+ - Refreshed stale Arena-gate example copy.
7
52
 
8
53
  ## 0.7.1
9
54
 
package/README.md CHANGED
@@ -9,8 +9,8 @@ line, with a public **Agent Arena** leaderboard ranked by realized paper PnL.
9
9
 
10
10
  **Plus a free prediction-market data surface — no key at all.** The same server
11
11
  ships four keyless `pm_data_*` tools serving CoinRithm's public cross-venue
12
- dataset: live odds across 10 venues (Polymarket, Kalshi, Smarkets, Limitless,
13
- Manifold, Metaculus, PredictIt, Rothera, Futuur, Myriad), cross-venue matches with a
12
+ dataset: live odds across 11 venues (Polymarket, Kalshi, Smarkets, Limitless,
13
+ Manifold, Metaculus, PredictIt, Rothera, Futuur, Myriad, ForecastEx), cross-venue matches with a
14
14
  liquidity-aware reference probability, a whale-trade tape, and market-wide
15
15
  volume stats ($60B+ all-time tracked). Point any MCP client at the hosted
16
16
  endpoint `https://mcp.coinrithm.com/mcp` and call them anonymously — the API
@@ -142,8 +142,8 @@ tool requires it. See [`DEPLOY.md`](./DEPLOY.md).
142
142
  | `pm_data_whales` | none (public) | `GET /api/prediction-markets/whales` |
143
143
 
144
144
  The four `pm_data_*` tools wrap CoinRithm's free public cross-venue dataset
145
- (all ten venues: Polymarket, Kalshi, Smarkets, Limitless, Manifold,
146
- Metaculus, PredictIt, Rothera, Futuur, Myriad). They require no API key, never attach yours, and
145
+ (all 11 venues: Polymarket, Kalshi, Smarkets, Limitless, Manifold,
146
+ Metaculus, PredictIt, Rothera, Futuur, Myriad, ForecastEx). They require no API key, never attach yours, and
147
147
  are research surfaces: `pm_data_events` list rows carry `referenceProbability`
148
148
  (a liquidity-aware cross-venue consensus on matched questions); `pm_data_event`
149
149
  includes `crossSourceMatches` (the same real-world question priced on other
@@ -195,9 +195,11 @@ with `runId` to export a private run-evidence bundle:
195
195
  The export includes a manifest and summary: first/last event time, venues,
196
196
  ledger statuses, quote/write/reject/replay counts, related paper-trade ids, and
197
197
  the sanitized ledger rows. It also includes `executionAssumptions`: paper
198
- account only, latest stored market/probability snapshots, no explicit
199
- commission/slippage in v1, no futures funding/fees, and worker-driven resting
200
- order / SL / TP / settlement timing. It is a reproducibility artifact for your
198
+ account only, latest stored market/probability snapshots, and the versioned
199
+ `paper_execution_v1` cost model (paper execution is **not costless** — fills
200
+ charge a modeled taker fee plus spread + slippage on spot/PM, disclosed per fill;
201
+ futures funding is not modeled), and worker-driven resting order / SL / TP /
202
+ settlement timing. It is a reproducibility artifact for your
201
203
  run; it is not a full point-in-time market archive and does not expose hidden
202
204
  reasoning. Aggregate audit stats include trace coverage for `runId` and
203
205
  `decisionId`. Run exports also include `retentionPolicy`: private ledger rows
@@ -1,4 +1,4 @@
1
- import { CoinRithmClient } from "./client.js";
1
+ import { CoinRithmClient, ProvenanceReport } from "./client.js";
2
2
  import { ProposedAction, AgentTrace, ApiResult, Observation, QuoteEvidence } from "./types.js";
3
3
  export declare function fetchQuote(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace?: AgentTrace): Promise<QuoteEvidence | undefined>;
4
- export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string): Promise<ApiResult>;
4
+ export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string, provenance?: ProvenanceReport): Promise<ApiResult>;
package/dist/agent/act.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // idempotency key.
4
4
  import { asObj, asNum, asStr } from "./extract.js";
5
5
  function coinIdFor(observation, symbol) {
6
- return observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined;
6
+ return (observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined);
7
7
  }
8
8
  function freshnessOf(block) {
9
9
  const fr = asObj(block.freshness);
@@ -17,7 +17,12 @@ export async function fetchQuote(client, action, observation, trace) {
17
17
  const coinId = coinIdFor(observation, action.symbol);
18
18
  if (!coinId)
19
19
  return { eligible: false, blockReasons: ["unresolved_symbol"] };
20
- r = await client.futuresQuote({ coinId, side: action.side, leverage: action.leverage, marginMusd: action.marginMusd }, trace);
20
+ r = await client.futuresQuote({
21
+ coinId,
22
+ side: action.side,
23
+ leverage: action.leverage,
24
+ marginMusd: action.marginMusd,
25
+ }, trace);
21
26
  }
22
27
  else if (action.type === "spot_order") {
23
28
  const coinId = coinIdFor(observation, action.symbol);
@@ -48,9 +53,17 @@ export async function fetchQuote(client, action, observation, trace) {
48
53
  estimatedCostMusd: asNum(d.estimatedCostMusd), // spot gross notional
49
54
  // Freshness lives in the response's `observation` block (anti-look-ahead).
50
55
  freshness: freshnessOf(asObj(d.observation)),
56
+ // PM open-time quality-gate preview (additive; older backends omit it → the
57
+ // runner just doesn't early-skip and falls through to the normal path).
58
+ openBlocked: d.openBlocked === true,
59
+ openBlockReasons: d.openBlockReasons,
51
60
  };
52
61
  }
53
- export async function executeAction(client, action, observation, trace, idempotencyKey) {
62
+ export async function executeAction(client, action, observation, trace, idempotencyKey,
63
+ // Optional SELF-REPORTED runner provenance, attached to a pm_open only (the one
64
+ // durable-artifact write path here). Absent => the request is byte-identical to
65
+ // pre-provenance behavior.
66
+ provenance) {
54
67
  if (action.type === "futures_open") {
55
68
  const coinId = coinIdFor(observation, action.symbol);
56
69
  if (!coinId)
@@ -114,6 +127,14 @@ export async function executeAction(client, action, observation, trace, idempote
114
127
  outcomeExternalMarketId: action.outcomeExternalMarketId,
115
128
  stakeMusd: action.stakeMusd,
116
129
  idempotencyKey,
130
+ // The agent's OWN independent forecast (already clamped/omitted by the runner).
131
+ // Included ONLY when present, so an absent forecast leaves the request body
132
+ // byte-identical to pre-forecast behavior.
133
+ ...(action.forecastProbability != null
134
+ ? { forecastProbability: action.forecastProbability }
135
+ : {}),
136
+ // Attach runner provenance only when present (byte-identical to before when absent).
137
+ ...(provenance ? { provenance } : {}),
117
138
  agentTrace: trace,
118
139
  });
119
140
  }
package/dist/agent/cli.js CHANGED
@@ -7,14 +7,14 @@
7
7
  import { mkdirSync, writeFileSync, existsSync, statSync, readFileSync, openSync, closeSync, unlinkSync, } from "node:fs";
8
8
  import { resolve as resolvePath, dirname, join, basename } from "node:path";
9
9
  import { parse as parseYaml } from "yaml";
10
- import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource } from "./resolve.js";
10
+ import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, } from "./resolve.js";
11
11
  import { buildSpec, loadAgent } from "./skill.js";
12
12
  import { validateSkill } from "./skillValidator.js";
13
13
  import { strictLint } from "./strictLint.js";
14
14
  import { checkCapabilityDrift } from "./capabilityGuard.js";
15
15
  import { buildManifest, writeManifest } from "./manifest.js";
16
16
  import { parseFrontmatter } from "./frontmatter.js";
17
- import { renderFolderOfOne, ejectFiles, PRESET_NAMES } from "./templates.js";
17
+ import { renderFolderOfOne, ejectFiles, PRESET_NAMES, } from "./templates.js";
18
18
  import { COINRITHM_API } from "./version.js";
19
19
  import { stableStringify, envFlag } from "./util.js";
20
20
  import { CoinRithmClient } from "./client.js";
@@ -62,7 +62,9 @@ export function cmdNew(targetPath, opts = {}) {
62
62
  }
63
63
  const preset = (opts.preset ?? "conservative");
64
64
  if (!PRESET_NAMES.includes(preset)) {
65
- return fail([`unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`]);
65
+ return fail([
66
+ `unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`,
67
+ ]);
66
68
  }
67
69
  const dir = resolvePath(targetPath);
68
70
  if (!dir || dir === resolvePath("."))
@@ -125,17 +127,25 @@ export function cmdLock(path) {
125
127
  ...drift.map((i) => ` [${i.code}] ${i.path ? `${i.path}: ` : ""}${i.message}`),
126
128
  ]
127
129
  : [];
128
- return { ok: true, code: 0, lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn] };
130
+ return {
131
+ ok: true,
132
+ code: 0,
133
+ lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn],
134
+ };
129
135
  }
130
136
  export function cmdEject(path) {
131
137
  const agentDir = agentDirOf(path);
132
138
  const abs = resolvePath(path);
133
- const keystone = existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "agent.md") : abs;
139
+ const keystone = existsSync(abs) && statSync(abs).isDirectory()
140
+ ? join(abs, "agent.md")
141
+ : abs;
134
142
  if (!existsSync(keystone))
135
143
  return fail([`no agent.md at ${keystone}`]);
136
144
  const { data: fm, body } = parseFrontmatter(readFileSync(keystone, "utf8"));
137
145
  if (Array.isArray(fm.extends)) {
138
- return fail(["agent already uses `extends` (already ejected?) — nothing to do"]);
146
+ return fail([
147
+ "agent already uses `extends` (already ejected?) — nothing to do",
148
+ ]);
139
149
  }
140
150
  const before = buildSpec(fm);
141
151
  const { files } = ejectFiles(fm, body);
@@ -149,13 +159,17 @@ export function cmdEject(path) {
149
159
  after = buildSpec(resolveAgent(agentDir).rawFrontmatter);
150
160
  }
151
161
  catch (e) {
152
- return fail([`ejected folder failed to re-resolve: ${e.message}`]);
162
+ return fail([
163
+ `ejected folder failed to re-resolve: ${e.message}`,
164
+ ]);
153
165
  }
154
166
  const same = stableStringify(before) === stableStringify(after);
155
167
  const lines = [
156
168
  `ejected into ${agentDir}`,
157
169
  ...Object.keys(files).map((f) => ` + ${f}`),
158
- same ? "✓ resolved spec unchanged" : "✗ WARNING: resolved spec CHANGED after eject",
170
+ same
171
+ ? "✓ resolved spec unchanged"
172
+ : "✗ WARNING: resolved spec CHANGED after eject",
159
173
  ];
160
174
  return { ok: same, code: same ? 0 : 1, lines };
161
175
  }
@@ -170,7 +184,10 @@ export function cmdInspect(path, json = false) {
170
184
  throw e;
171
185
  }
172
186
  const spec = buildSpec(resolved.rawFrontmatter);
173
- const lint = [...strictLint(resolved.rawFrontmatter), ...checkCapabilityDrift(resolved, spec)];
187
+ const lint = [
188
+ ...strictLint(resolved.rawFrontmatter),
189
+ ...checkCapabilityDrift(resolved, spec),
190
+ ];
174
191
  const v = validateSkill({ spec, body: resolved.mergedProse, raw: resolved.rawFrontmatter }, "self-host");
175
192
  const output = {
176
193
  resolvedConfig: resolved.rawFrontmatter,
@@ -179,7 +196,12 @@ export function cmdInspect(path, json = false) {
179
196
  validation: { valid: v.valid, issues: v.issues, lint },
180
197
  };
181
198
  if (json) {
182
- return { ok: v.valid, code: 0, lines: [JSON.stringify(output, null, 2)], data: output };
199
+ return {
200
+ ok: v.valid,
201
+ code: 0,
202
+ lines: [JSON.stringify(output, null, 2)],
203
+ data: output,
204
+ };
183
205
  }
184
206
  const lines = [
185
207
  `name: ${spec.name}`,
@@ -278,7 +300,9 @@ export async function cmdRun(path, opts = {}) {
278
300
  }
279
301
  const apiKey = process.env.COINRITHM_API_KEY;
280
302
  if (!apiKey)
281
- return fail(["COINRITHM_API_KEY is not set (needed to read your paper account)"]);
303
+ return fail([
304
+ "COINRITHM_API_KEY is not set (needed to read your paper account)",
305
+ ]);
282
306
  let provider;
283
307
  try {
284
308
  provider = selectProvider(loaded.spec, process.env, fetch);
@@ -286,11 +310,16 @@ export async function cmdRun(path, opts = {}) {
286
310
  catch (e) {
287
311
  return fail([e.message]);
288
312
  }
289
- const client = new CoinRithmClient({ apiKey, baseUrl: process.env.COINRITHM_API_URL });
313
+ const client = new CoinRithmClient({
314
+ apiKey,
315
+ baseUrl: process.env.COINRITHM_API_URL,
316
+ });
290
317
  const stateFile = opts.stateFile ?? join(agentDirOf(path), ".agent.state.json");
291
318
  const release = acquireLock(stateFile);
292
319
  if (!release) {
293
- return fail([`another runner holds ${stateFile}.lock — only one runner per agent at a time`]);
320
+ return fail([
321
+ `another runner holds ${stateFile}.lock — only one runner per agent at a time`,
322
+ ]);
294
323
  }
295
324
  // A self-host `run` is a cadence-paced loop users stop with Ctrl-C. Node exits
296
325
  // on SIGINT/SIGTERM WITHOUT unwinding the finally across the awaited loop, so
@@ -315,7 +344,9 @@ export async function cmdRun(path, opts = {}) {
315
344
  return fail([e.message]);
316
345
  }
317
346
  if (state.disabled) {
318
- return fail([`agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`]);
347
+ return fail([
348
+ `agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`,
349
+ ]);
319
350
  }
320
351
  const live = !!opts.live;
321
352
  const lines = [
@@ -399,7 +430,10 @@ export async function main(argv) {
399
430
  let r;
400
431
  switch (cmd) {
401
432
  case "new":
402
- r = cmdNew(pos[0] ?? "", { template: flags.template, preset: flags.preset });
433
+ r = cmdNew(pos[0] ?? "", {
434
+ template: flags.template,
435
+ preset: flags.preset,
436
+ });
403
437
  break;
404
438
  case "validate":
405
439
  r = cmdValidate(pos[0] ?? ".", flags.hosted ? "hosted" : "self-host");
@@ -416,7 +450,11 @@ export async function main(argv) {
416
450
  case "run": {
417
451
  // dry-run is the default; only --live (or LIVE=1) AND not --dry-run trades.
418
452
  const live = (!!flags.live || process.env.LIVE === "1") && !flags.dryRun;
419
- r = await cmdRun(pos[0] ?? ".", { once: flags.once, live, stateFile: flags.state });
453
+ r = await cmdRun(pos[0] ?? ".", {
454
+ once: flags.once,
455
+ live,
456
+ stateFile: flags.state,
457
+ });
420
458
  break;
421
459
  }
422
460
  case undefined:
@@ -426,10 +464,13 @@ export async function main(argv) {
426
464
  r = { ok: true, code: 0, lines: usageLines() };
427
465
  break;
428
466
  default:
429
- r = { ok: false, code: 1, lines: [`unknown command "${cmd}"`, ...usageLines()] };
467
+ r = {
468
+ ok: false,
469
+ code: 1,
470
+ lines: [`unknown command "${cmd}"`, ...usageLines()],
471
+ };
430
472
  }
431
473
  for (const line of r.lines) {
432
- // eslint-disable-next-line no-console
433
474
  console.log(line);
434
475
  }
435
476
  return r.code;
@@ -1,5 +1,20 @@
1
1
  import { AgentTrace, ApiResult } from "./types.js";
2
2
  export declare const DEFAULT_BASE_URL = "https://api.coinrithm.com";
3
+ export type ProvenanceReport = {
4
+ runtimeKind?: "hosted_scheduler" | "self_host_runner" | "byo_api" | "mcp_tool";
5
+ packageVersion?: string;
6
+ bundleId?: string;
7
+ bundleVersion?: string;
8
+ skillVersions?: Record<string, string>;
9
+ promptHash?: string;
10
+ configHash?: string;
11
+ modelProvider?: string;
12
+ modelName?: string;
13
+ evidenceRef?: {
14
+ snapshotIds?: string[];
15
+ sourceCapturedAt?: string;
16
+ };
17
+ };
3
18
  export interface ClientConfig {
4
19
  apiKey: string;
5
20
  baseUrl?: string;
@@ -107,8 +122,26 @@ export declare class CoinRithmClient {
107
122
  outcomeExternalMarketId: string;
108
123
  stakeMusd: number;
109
124
  idempotencyKey: string;
125
+ forecastProbability?: number;
126
+ provenance?: ProvenanceReport;
110
127
  agentTrace?: AgentTrace;
111
128
  }): Promise<ApiResult>;
129
+ reportPmOpportunity(body: {
130
+ kind: "abstained" | "forecast_only" | "quote_expired";
131
+ source?: string;
132
+ slug?: string;
133
+ outcomeExternalMarketId?: string;
134
+ forecastProbability?: number;
135
+ marketProbability?: number;
136
+ reasonCode?: string;
137
+ cohort?: {
138
+ universeSize?: number;
139
+ horizon?: string;
140
+ };
141
+ decisionId?: string | null;
142
+ runId?: string | null;
143
+ provenance?: ProvenanceReport;
144
+ }, trace?: AgentTrace): Promise<ApiResult>;
112
145
  exportRunEvidence(runId: string): Promise<ApiResult>;
113
146
  }
114
147
  export declare function isFailClosed(status: number): boolean;
@@ -64,7 +64,10 @@ export class CoinRithmClient {
64
64
  return {
65
65
  ok: false,
66
66
  status: 0,
67
- data: { error: "network_error", message: err instanceof Error ? err.message : String(err) },
67
+ data: {
68
+ error: "network_error",
69
+ message: err instanceof Error ? err.message : String(err),
70
+ },
68
71
  };
69
72
  }
70
73
  const retryAfter = Number(res.headers.get("retry-after"));
@@ -88,7 +91,9 @@ export class CoinRithmClient {
88
91
  ok: res.ok,
89
92
  status: res.status,
90
93
  data,
91
- retryAfterSeconds: res.status === 429 && Number.isFinite(retryAfter) ? retryAfter : undefined,
94
+ retryAfterSeconds: res.status === 429 && Number.isFinite(retryAfter)
95
+ ? retryAfter
96
+ : undefined,
92
97
  rateLimitRemaining: Number(res.headers.get("ratelimit-remaining")) || undefined,
93
98
  ledgerEventId: res.headers.get("x-coinrithm-ledger-event-id"),
94
99
  };
@@ -120,17 +125,24 @@ export class CoinRithmClient {
120
125
  return this.request("GET", "/api/agent/trades", { query, trace });
121
126
  }
122
127
  futuresPositions(query, trace) {
123
- return this.request("GET", "/api/agent/positions/futures", { query, trace });
128
+ return this.request("GET", "/api/agent/positions/futures", {
129
+ query,
130
+ trace,
131
+ });
124
132
  }
125
133
  futuresQuote(body, trace) {
126
- return this.request("POST", "/api/agent/futures/quote", { body: { ...body, agentTrace: trace } });
134
+ return this.request("POST", "/api/agent/futures/quote", {
135
+ body: { ...body, agentTrace: trace },
136
+ });
127
137
  }
128
138
  // ── spot ─────────────────────────────────────────────────────────────────
129
139
  openOrders(query, trace) {
130
140
  return this.request("GET", "/api/agent/orders/open", { query, trace });
131
141
  }
132
142
  spotQuote(body, trace) {
133
- return this.request("POST", "/api/agent/spot/quote", { body: { ...body, agentTrace: trace } });
143
+ return this.request("POST", "/api/agent/spot/quote", {
144
+ body: { ...body, agentTrace: trace },
145
+ });
134
146
  }
135
147
  // ── prediction markets ───────────────────────────────────────────────────
136
148
  discoverPmMarkets(query, trace) {
@@ -149,7 +161,9 @@ export class CoinRithmClient {
149
161
  return this.request("GET", "/api/agent/positions/pm", { query, trace });
150
162
  }
151
163
  pmQuote(body, trace) {
152
- return this.request("POST", "/api/agent/pm/quote", { body: { ...body, agentTrace: trace } });
164
+ return this.request("POST", "/api/agent/pm/quote", {
165
+ body: { ...body, agentTrace: trace },
166
+ });
153
167
  }
154
168
  // ── writes ─────────────────────────────────────────────────────────────────
155
169
  openFutures(body) {
@@ -173,9 +187,22 @@ export class CoinRithmClient {
173
187
  openPmPosition(body) {
174
188
  return this.request("POST", "/api/agent/pm/open", { body });
175
189
  }
190
+ // Report a NON-opened PM opportunity (abstained / forecast_only / quote_expired)
191
+ // so the public evaluation captures the FULL opportunity universe, not only
192
+ // opened trades. EVIDENCE, not a trade: needs only the read scope and does not
193
+ // move a wallet/position. Best-effort at the call site — a failure never affects
194
+ // the cycle. decisionId is the per-cycle idempotency key (server dedupes on
195
+ // (apiKeyId, decisionId)).
196
+ reportPmOpportunity(body, trace) {
197
+ return this.request("POST", "/api/agent/pm/opportunity", {
198
+ body: { ...body, agentTrace: trace },
199
+ });
200
+ }
176
201
  // Run-evidence export — runId is URL-encoded into the query.
177
202
  exportRunEvidence(runId) {
178
- return this.request("GET", "/api/agent/ledger/export", { query: { runId } });
203
+ return this.request("GET", "/api/agent/ledger/export", {
204
+ query: { runId },
205
+ });
179
206
  }
180
207
  }
181
208
  // 401/403/409/422 are terminal, fail-closed outcomes for a cycle (auth/scope,
@@ -111,12 +111,14 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
111
111
  stakeMusd: z.ZodEffects<z.ZodTypeAny, any, unknown>;
112
112
  confidence: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>, any, unknown>;
113
113
  rationaleSummary: z.ZodOptional<z.ZodString>;
114
+ forecastProbability: z.ZodOptional<z.ZodEffects<z.ZodAny, number | undefined, any>>;
114
115
  }, "strict", z.ZodTypeAny, {
115
116
  type: "pm_open";
116
117
  source?: string | undefined;
117
118
  slug?: string | undefined;
118
119
  outcomeExternalMarketId?: string | undefined;
119
120
  stakeMusd?: any;
121
+ forecastProbability?: number | undefined;
120
122
  confidence?: any;
121
123
  rationaleSummary?: string | undefined;
122
124
  ref?: string | undefined;
@@ -126,6 +128,7 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
126
128
  slug?: string | undefined;
127
129
  outcomeExternalMarketId?: string | undefined;
128
130
  stakeMusd?: unknown;
131
+ forecastProbability?: any;
129
132
  confidence?: unknown;
130
133
  rationaleSummary?: string | undefined;
131
134
  ref?: string | undefined;
@@ -8,12 +8,29 @@ import { z } from "zod";
8
8
  // validating; leave anything else untouched so genuine garbage ("abc", "pos#5")
9
9
  // still fails closed. This is why actions were being rejected with
10
10
  // "actions.0.positionId: Expected number, received string".
11
- const num = (inner) => z.preprocess((v) => (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : v), inner);
11
+ const num = (inner) => z.preprocess((v) => typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))
12
+ ? Number(v)
13
+ : v, inner);
12
14
  // Optional 0..1 confidence, tolerant of stringified/null input.
13
15
  const confidence = num(z.number().min(0).max(1))
14
16
  .nullable()
15
17
  .optional()
16
18
  .transform((v) => v ?? undefined);
19
+ // The model's OWN 0-100 forecast for the backed PM side (its independent
20
+ // probability the side wins, decided from the question — NOT the market price).
21
+ // Deliberately TOLERANT: a missing / null / non-numeric value becomes undefined
22
+ // (the runner then omits the field) rather than failing the whole action. A bad
23
+ // forecast must NEVER block an otherwise-valid trade. Out-of-range values are NOT
24
+ // rejected here — the runner clamps them to the backend's [1,99] rail. Kept as a
25
+ // permissive `any→number|undefined` so it can never throw inside the strict
26
+ // discriminated union.
27
+ const forecastProbability = z
28
+ .any()
29
+ .transform((v) => {
30
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
31
+ return typeof n === "number" && Number.isFinite(n) ? n : undefined;
32
+ })
33
+ .optional();
17
34
  const futuresOpen = z
18
35
  .object({
19
36
  type: z.literal("futures_open"),
@@ -79,6 +96,7 @@ const pmOpen = z
79
96
  stakeMusd: num(z.number().positive()),
80
97
  confidence,
81
98
  rationaleSummary: z.string().optional(),
99
+ forecastProbability,
82
100
  })
83
101
  .strict();
84
102
  export const actionSchema = z.discriminatedUnion("type", [
@@ -164,13 +182,18 @@ export function parseDecision(text) {
164
182
  obj = normalizeDecisionVerb(coerceJson(text));
165
183
  }
166
184
  catch (err) {
167
- return { ok: false, error: `model output is not valid JSON: ${err instanceof Error ? err.message : String(err)}` };
185
+ return {
186
+ ok: false,
187
+ error: `model output is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
188
+ };
168
189
  }
169
190
  const res = decisionSchema.safeParse(obj);
170
191
  if (!res.success) {
171
192
  return {
172
193
  ok: false,
173
- error: res.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
194
+ error: res.error.issues
195
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
196
+ .join("; "),
174
197
  };
175
198
  }
176
199
  const d = res.data;
@@ -21,7 +21,8 @@ export function validateAction(action, ctx) {
21
21
  // throttled (we want an active Arena), and hosted agents only when the customer sets a
22
22
  // positive cap. The risk caps below (daily loss, open margin, leverage, stops) are the
23
23
  // real guardrails and always apply regardless of the trade-count cap.
24
- if (spec.limits.maxTradesPerDay > 0 && ctx.writesToday >= spec.limits.maxTradesPerDay) {
24
+ if (spec.limits.maxTradesPerDay > 0 &&
25
+ ctx.writesToday >= spec.limits.maxTradesPerDay) {
25
26
  return fail("daily_trade_cap", `maxTradesPerDay ${spec.limits.maxTradesPerDay} reached`);
26
27
  }
27
28
  // Deny-list: an open on a blocked symbol is rejected up front (deny wins over
@@ -8,14 +8,34 @@
8
8
  // agent runs on a default tier and this still enforces it.)
9
9
  // Effective caps per tier. Tightenable later from config; these are the defaults.
10
10
  export const TIER_LIMITS = {
11
- free_demo: { maxLlmCallsPerHour: 4, minCadenceSeconds: 3600, maxConcurrentAgents: 1 },
12
- builder: { maxLlmCallsPerHour: 20, minCadenceSeconds: 900, maxConcurrentAgents: 3 },
13
- pro: { maxLlmCallsPerHour: 120, minCadenceSeconds: 60, maxConcurrentAgents: 10 },
11
+ free_demo: {
12
+ maxLlmCallsPerHour: 4,
13
+ minCadenceSeconds: 3600,
14
+ maxConcurrentAgents: 1,
15
+ },
16
+ builder: {
17
+ maxLlmCallsPerHour: 20,
18
+ minCadenceSeconds: 900,
19
+ maxConcurrentAgents: 3,
20
+ },
21
+ pro: {
22
+ maxLlmCallsPerHour: 120,
23
+ minCadenceSeconds: 60,
24
+ maxConcurrentAgents: 10,
25
+ },
14
26
  // BYO key = the user's own model quota, so we don't cap their calls; we still
15
27
  // host + meter + verify (that's what they pay the infra fee for).
16
- byok: { maxLlmCallsPerHour: 0, minCadenceSeconds: 60, maxConcurrentAgents: 5 },
28
+ byok: {
29
+ maxLlmCallsPerHour: 0,
30
+ minCadenceSeconds: 60,
31
+ maxConcurrentAgents: 5,
32
+ },
17
33
  // The house showcase fleet — uncapped, runs on our pooled keys.
18
- house: { maxLlmCallsPerHour: 0, minCadenceSeconds: 60, maxConcurrentAgents: 0 },
34
+ house: {
35
+ maxLlmCallsPerHour: 0,
36
+ minCadenceSeconds: 60,
37
+ maxConcurrentAgents: 0,
38
+ },
19
39
  };
20
40
  // Tighten one numeric cap: 0 means "unlimited" on either side. The tier always wins
21
41
  // where it imposes a finite cap; it can never raise a request.
@@ -1,5 +1,5 @@
1
1
  export { runCycle, type RunnerDeps } from "./runner.js";
2
- export { selectProvider, type ProviderEnv, type Provider } from "./providers.js";
2
+ export { selectProvider, type ProviderEnv, type Provider, } from "./providers.js";
3
3
  export { CoinRithmClient } from "./client.js";
4
4
  export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
5
5
  export { resolveAgent } from "./resolve.js";
@@ -7,4 +7,5 @@ export { validateSkill, type SkillValidationMode } from "./skillValidator.js";
7
7
  export { newState, rollDay } from "./state.js";
8
8
  export { makeRunId } from "./runEvidence.js";
9
9
  export { parseCadenceMs } from "./util.js";
10
+ export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, type BenchmarkStrategy, type BenchmarkAgentDefinition, } from "./mechanical.js";
10
11
  export type { AgentSpec, RunState, CycleResult, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.js";
@@ -6,7 +6,7 @@
6
6
  // This barrel is the ONE import a host scheduler needs; it re-exports only the
7
7
  // stable engine pieces, never the CLI.
8
8
  export { runCycle } from "./runner.js";
9
- export { selectProvider } from "./providers.js";
9
+ export { selectProvider, } from "./providers.js";
10
10
  export { CoinRithmClient } from "./client.js";
11
11
  export { loadAgent, buildSpec } from "./skill.js";
12
12
  export { resolveAgent } from "./resolve.js";
@@ -14,3 +14,6 @@ export { validateSkill } from "./skillValidator.js";
14
14
  export { newState, rollDay } from "./state.js";
15
15
  export { makeRunId } from "./runEvidence.js";
16
16
  export { parseCadenceMs } from "./util.js";
17
+ // Mechanical BENCHMARK baseline agents (sol #7): the deterministic, non-LLM
18
+ // reference line the scheduler seeds into agent_runtime and the runner executes.
19
+ export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, } from "./mechanical.js";
@@ -1,5 +1,7 @@
1
1
  // Defensive extractors for untyped API JSON.
2
- export const asObj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2
+ export const asObj = (v) => v && typeof v === "object" && !Array.isArray(v)
3
+ ? v
4
+ : {};
3
5
  export const asArr = (v) => (Array.isArray(v) ? v : []);
4
6
  export const asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
5
7
  export const asStr = (v) => typeof v === "string" ? v : undefined;