@coinrithm/mcp-trading 0.7.15 → 0.7.17

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 (76) hide show
  1. package/CHANGELOG.md +885 -692
  2. package/README.md +486 -400
  3. package/dist/agent/bench/bench.d.ts +59 -0
  4. package/dist/agent/bench/bench.js +319 -0
  5. package/dist/agent/bench/cassette.d.ts +97 -0
  6. package/dist/agent/bench/cassette.js +181 -0
  7. package/dist/agent/bench/costs.d.ts +28 -0
  8. package/dist/agent/bench/costs.js +60 -0
  9. package/dist/agent/bench/labelBuilder.d.ts +123 -0
  10. package/dist/agent/bench/labelBuilder.js +282 -0
  11. package/dist/agent/bench/labels.d.ts +75 -0
  12. package/dist/agent/bench/labels.js +259 -0
  13. package/dist/agent/bench/recordingClient.d.ts +51 -0
  14. package/dist/agent/bench/recordingClient.js +193 -0
  15. package/dist/agent/bench/replayClient.d.ts +41 -0
  16. package/dist/agent/bench/replayClient.js +262 -0
  17. package/dist/agent/bench/report.d.ts +115 -0
  18. package/dist/agent/bench/report.js +435 -0
  19. package/dist/agent/capitalSizing.d.ts +10 -1
  20. package/dist/agent/capitalSizing.js +28 -4
  21. package/dist/agent/cli.d.ts +25 -0
  22. package/dist/agent/cli.js +300 -2
  23. package/dist/agent/client.d.ts +14 -0
  24. package/dist/agent/client.js +23 -6
  25. package/dist/agent/configurationWarnings.js +11 -0
  26. package/dist/agent/decision.d.ts +24 -16
  27. package/dist/agent/decision.js +92 -3
  28. package/dist/agent/decisionValidator.d.ts +31 -0
  29. package/dist/agent/decisionValidator.js +148 -3
  30. package/dist/agent/engine.d.ts +5 -3
  31. package/dist/agent/engine.js +5 -3
  32. package/dist/agent/futuresCloseOutcome.d.ts +19 -0
  33. package/dist/agent/futuresCloseOutcome.js +54 -0
  34. package/dist/agent/futuresEligibility.d.ts +13 -0
  35. package/dist/agent/futuresEligibility.js +43 -5
  36. package/dist/agent/observe.d.ts +28 -2
  37. package/dist/agent/observe.js +530 -90
  38. package/dist/agent/pmPaperCandidates.d.ts +17 -0
  39. package/dist/agent/pmPaperCandidates.js +125 -0
  40. package/dist/agent/pmPaperCycle.d.ts +16 -0
  41. package/dist/agent/pmPaperCycle.js +372 -0
  42. package/dist/agent/pmPaperDecision.d.ts +187 -0
  43. package/dist/agent/pmPaperDecision.js +101 -0
  44. package/dist/agent/pmPaperPolicy.d.ts +71 -0
  45. package/dist/agent/pmPaperPolicy.js +46 -0
  46. package/dist/agent/prompt.d.ts +15 -1
  47. package/dist/agent/prompt.js +187 -9
  48. package/dist/agent/promptTables.d.ts +1 -0
  49. package/dist/agent/promptTables.js +33 -0
  50. package/dist/agent/providerCapabilities.d.ts +7 -0
  51. package/dist/agent/providerCapabilities.js +27 -1
  52. package/dist/agent/providers.d.ts +26 -7
  53. package/dist/agent/providers.js +193 -48
  54. package/dist/agent/resolve.js +3 -0
  55. package/dist/agent/runner.d.ts +17 -1
  56. package/dist/agent/runner.js +210 -38
  57. package/dist/agent/setups.d.ts +2 -1
  58. package/dist/agent/setups.js +17 -16
  59. package/dist/agent/signals.d.ts +33 -0
  60. package/dist/agent/signals.js +89 -0
  61. package/dist/agent/skill.js +46 -0
  62. package/dist/agent/skillValidator.js +56 -0
  63. package/dist/agent/strictLint.js +30 -0
  64. package/dist/agent/templates.js +30 -30
  65. package/dist/agent/types.d.ts +180 -12
  66. package/dist/agent/universe.d.ts +78 -0
  67. package/dist/agent/universe.js +215 -0
  68. package/dist/agent/version.d.ts +1 -1
  69. package/dist/agent/version.js +1 -1
  70. package/dist/client.d.ts +14 -0
  71. package/dist/client.js +31 -1
  72. package/dist/http.js +1 -1
  73. package/dist/pmPaperV2.d.ts +81 -0
  74. package/dist/pmPaperV2.js +53 -0
  75. package/dist/tools.js +84 -6
  76. package/package.json +100 -97
@@ -0,0 +1,78 @@
1
+ import type { UniverseConfig } from "./types.js";
2
+ export declare const UNIVERSE_SORTS: readonly ["gainers_24h", "losers_24h", "abs_change_24h", "abs_change_1h", "volume_24h", "rank"];
3
+ export type UniverseSort = (typeof UNIVERSE_SORTS)[number];
4
+ export declare const UNIVERSE_DEFAULTS: {
5
+ rankMin: number;
6
+ rankMax: number;
7
+ minVolume24hUsd: number;
8
+ sort: UniverseSort;
9
+ resolveTop: number;
10
+ scanLimit: number;
11
+ };
12
+ export declare const UNIVERSE_MAX_RESOLVE_TOP = 10;
13
+ export declare const UNIVERSE_MAX_SCAN_LIMIT = 50;
14
+ /** True when the agent discovers candidates beyond its watchlist. */
15
+ export declare function scansUniverse(spec: {
16
+ capabilities: readonly string[];
17
+ universe?: UniverseConfig;
18
+ }): boolean;
19
+ export interface UniverseQuery {
20
+ rankMin: number;
21
+ rankMax: number;
22
+ minVolume24hUsd: number;
23
+ excludeStablecoins: boolean;
24
+ includeSectors: string[];
25
+ excludeSectors: string[];
26
+ sort: UniverseSort;
27
+ limit: number;
28
+ }
29
+ /** The screener query a validated universe block asks for. */
30
+ export declare function universeQuery(u: UniverseConfig): UniverseQuery;
31
+ /** How many screener rows become full watch entries this cycle. */
32
+ export declare function universeResolveTop(u: UniverseConfig): number;
33
+ /** Wire form: csv sectors, string numbers; empty filters are omitted. With
34
+ * `ucids`, the same boundaries are applied to just those coins (membership). */
35
+ export declare function universeQueryParams(q: UniverseQuery, ucids?: readonly string[]): Record<string, string | number>;
36
+ export interface UniverseRow {
37
+ symbol: string;
38
+ coinId?: string;
39
+ name?: string;
40
+ slug?: string;
41
+ marketCapRank?: number;
42
+ priceUsd?: number;
43
+ change1h?: number;
44
+ change24h?: number;
45
+ change7d?: number;
46
+ volume24hUsd?: number;
47
+ sectors?: string[];
48
+ }
49
+ /** Keep malformed evidence unknown; dropping bad elements can widen a screen. */
50
+ export declare function universeSectorsOf(raw: unknown): string[] | undefined;
51
+ /**
52
+ * Defence in depth on server rows (never widen): a row is kept only when its
53
+ * own fields PROVE it is inside every enabled boundary. The rank band is
54
+ * always enabled, so a row without a rank is dropped; a volume floor needs a
55
+ * volume; any sector rule (stablecoins out, sectors out, sectors in) needs the
56
+ * row's sector list. Missing or malformed evidence drops the row (root review
57
+ * 2026-10-07: unverifiable is not inside).
58
+ */
59
+ export declare function filterUniverseRows(rows: UniverseRow[], q: UniverseQuery): UniverseRow[];
60
+ /**
61
+ * Entry eligibility under declared boundaries (root review 2026-10-07): with a
62
+ * `universe` block, a NEW position may only be opened on a coin proven inside
63
+ * the boundaries this cycle: a discovered row, or a watchlist coin the
64
+ * screener confirmed (`withinBoundaries: true`). Anything else (outside, or
65
+ * unverifiable because the check failed) stays visible as context and can
66
+ * still be closed or sold, but not entered. Returns the rejection reason, or
67
+ * null when the entry is allowed. No universe block = no extra rule.
68
+ */
69
+ export declare function universeEntryBlock(spec: {
70
+ universe?: UniverseConfig;
71
+ }, entry: {
72
+ discovered?: boolean;
73
+ withinBoundaries?: boolean;
74
+ }): string | null;
75
+ /** Validation issues for a raw `universe` block ([] when valid or absent). */
76
+ export declare function universeIssues(raw: unknown): Array<[string, string]>;
77
+ /** One line for the prompt: the boundaries in plain words. */
78
+ export declare function describeUniverse(u: UniverseConfig): string;
@@ -0,0 +1,215 @@
1
+ // Agent market BOUNDARIES (owner 2026-10-07, Telegram 56641/56644). Instead of
2
+ // a fixed coin list, a bundle declares where its agent may look:
3
+ //
4
+ // universe:
5
+ // rank: { min: 1, max: 100 } # market-cap rank band
6
+ // minVolume24hUsd: 1000000 # liquidity floor
7
+ // excludeStablecoins: true
8
+ // includeSectors: [defi, ai] # curated ids, GET /api/agent/universe/sectors
9
+ // excludeSectors: [meme]
10
+ // sort: abs_change_24h # what "an opportunity" means first
11
+ // resolveTop: 6 # rows resolved into full watch entries
12
+ //
13
+ // Each cycle observe() asks GET /api/agent/universe for the rows inside those
14
+ // boundaries, resolves the top few into full watch entries (marked
15
+ // `discovered: true`) and passes the rest as compact context. The validator's
16
+ // tradable set is watch membership, so a discovered row is tradable for that
17
+ // cycle and every cap still applies; the blocklist still wins. The server
18
+ // filters, and filterUniverseRows re-checks what the row says about itself,
19
+ // so a wrong or older server can narrow the set but never widen it.
20
+ export const UNIVERSE_SORTS = [
21
+ "gainers_24h",
22
+ "losers_24h",
23
+ "abs_change_24h",
24
+ "abs_change_1h",
25
+ "volume_24h",
26
+ "rank",
27
+ ];
28
+ export const UNIVERSE_DEFAULTS = {
29
+ rankMin: 1,
30
+ rankMax: 100,
31
+ minVolume24hUsd: 0,
32
+ sort: "abs_change_24h",
33
+ resolveTop: 6,
34
+ scanLimit: 15,
35
+ };
36
+ export const UNIVERSE_MAX_RESOLVE_TOP = 10;
37
+ export const UNIVERSE_MAX_SCAN_LIMIT = 50;
38
+ const SECTOR_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
39
+ /** True when the agent discovers candidates beyond its watchlist. */
40
+ export function scansUniverse(spec) {
41
+ return (spec.universe !== undefined || spec.capabilities.includes("universe_scan"));
42
+ }
43
+ /** The screener query a validated universe block asks for. */
44
+ export function universeQuery(u) {
45
+ return {
46
+ rankMin: u.rank?.min ?? UNIVERSE_DEFAULTS.rankMin,
47
+ rankMax: u.rank?.max ?? UNIVERSE_DEFAULTS.rankMax,
48
+ minVolume24hUsd: u.minVolume24hUsd ?? UNIVERSE_DEFAULTS.minVolume24hUsd,
49
+ excludeStablecoins: u.excludeStablecoins ?? false,
50
+ includeSectors: [...(u.includeSectors ?? [])],
51
+ excludeSectors: [...(u.excludeSectors ?? [])],
52
+ sort: u.sort ?? UNIVERSE_DEFAULTS.sort,
53
+ limit: u.scanLimit ?? UNIVERSE_DEFAULTS.scanLimit,
54
+ };
55
+ }
56
+ /** How many screener rows become full watch entries this cycle. */
57
+ export function universeResolveTop(u) {
58
+ return Math.min(u.resolveTop ?? UNIVERSE_DEFAULTS.resolveTop, UNIVERSE_MAX_RESOLVE_TOP);
59
+ }
60
+ /** Wire form: csv sectors, string numbers; empty filters are omitted. With
61
+ * `ucids`, the same boundaries are applied to just those coins (membership). */
62
+ export function universeQueryParams(q, ucids) {
63
+ return {
64
+ ...(ucids && ucids.length > 0 ? { ucids: ucids.join(",") } : {}),
65
+ rankMin: q.rankMin,
66
+ rankMax: q.rankMax,
67
+ minVolume24hUsd: q.minVolume24hUsd,
68
+ excludeStablecoins: q.excludeStablecoins ? "true" : "false",
69
+ ...(q.includeSectors.length
70
+ ? { includeSectors: q.includeSectors.join(",") }
71
+ : {}),
72
+ ...(q.excludeSectors.length
73
+ ? { excludeSectors: q.excludeSectors.join(",") }
74
+ : {}),
75
+ sort: q.sort,
76
+ limit: q.limit,
77
+ };
78
+ }
79
+ /** Keep malformed evidence unknown; dropping bad elements can widen a screen. */
80
+ export function universeSectorsOf(raw) {
81
+ return Array.isArray(raw) &&
82
+ raw.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s))
83
+ ? raw
84
+ : undefined;
85
+ }
86
+ /**
87
+ * Defence in depth on server rows (never widen): a row is kept only when its
88
+ * own fields PROVE it is inside every enabled boundary. The rank band is
89
+ * always enabled, so a row without a rank is dropped; a volume floor needs a
90
+ * volume; any sector rule (stablecoins out, sectors out, sectors in) needs the
91
+ * row's sector list. Missing or malformed evidence drops the row (root review
92
+ * 2026-10-07: unverifiable is not inside).
93
+ */
94
+ export function filterUniverseRows(rows, q) {
95
+ const excluded = new Set([
96
+ ...q.excludeSectors,
97
+ ...(q.excludeStablecoins ? ["stablecoins"] : []),
98
+ ]);
99
+ const included = q.includeSectors.filter((s) => !excluded.has(s));
100
+ const sectorRules = excluded.size > 0 || q.includeSectors.length > 0;
101
+ return rows.filter((r) => {
102
+ const rank = r.marketCapRank;
103
+ if (typeof rank !== "number" || !Number.isFinite(rank))
104
+ return false;
105
+ if (rank < q.rankMin || rank > q.rankMax)
106
+ return false;
107
+ if (q.minVolume24hUsd > 0) {
108
+ const vol = r.volume24hUsd;
109
+ if (typeof vol !== "number" || !Number.isFinite(vol))
110
+ return false;
111
+ if (vol < q.minVolume24hUsd)
112
+ return false;
113
+ }
114
+ if (sectorRules) {
115
+ const sectors = universeSectorsOf(r.sectors);
116
+ if (sectors === undefined)
117
+ return false;
118
+ if (sectors.some((s) => excluded.has(s)))
119
+ return false;
120
+ if (q.includeSectors.length > 0 &&
121
+ !sectors.some((s) => included.includes(s)))
122
+ return false;
123
+ }
124
+ return true;
125
+ });
126
+ }
127
+ /**
128
+ * Entry eligibility under declared boundaries (root review 2026-10-07): with a
129
+ * `universe` block, a NEW position may only be opened on a coin proven inside
130
+ * the boundaries this cycle: a discovered row, or a watchlist coin the
131
+ * screener confirmed (`withinBoundaries: true`). Anything else (outside, or
132
+ * unverifiable because the check failed) stays visible as context and can
133
+ * still be closed or sold, but not entered. Returns the rejection reason, or
134
+ * null when the entry is allowed. No universe block = no extra rule.
135
+ */
136
+ export function universeEntryBlock(spec, entry) {
137
+ if (!spec.universe)
138
+ return null;
139
+ if (entry.discovered === true || entry.withinBoundaries === true)
140
+ return null;
141
+ return entry.withinBoundaries === false
142
+ ? "outside the declared market boundaries"
143
+ : "not verified inside the declared market boundaries this cycle";
144
+ }
145
+ /** Validation issues for a raw `universe` block ([] when valid or absent). */
146
+ export function universeIssues(raw) {
147
+ if (raw === undefined)
148
+ return [];
149
+ const issues = [];
150
+ const add = (code, reason) => issues.push([code, reason]);
151
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
152
+ add("skill_universe", "universe must be a mapping");
153
+ return issues;
154
+ }
155
+ const u = raw;
156
+ const int = (v, lo, hi) => typeof v === "number" && Number.isInteger(v) && v >= lo && v <= hi;
157
+ if (u.rank !== undefined) {
158
+ const rank = u.rank;
159
+ if (rank === null || typeof rank !== "object" || Array.isArray(rank)) {
160
+ add("skill_universe_rank", "universe.rank must be { min, max }");
161
+ }
162
+ else {
163
+ if (rank.min !== undefined && !int(rank.min, 1, 10_000))
164
+ add("skill_universe_rank", "universe.rank.min must be a whole number between 1 and 10000");
165
+ if (rank.max !== undefined && !int(rank.max, 1, 10_000))
166
+ add("skill_universe_rank", "universe.rank.max must be a whole number between 1 and 10000");
167
+ const min = rank.min ?? UNIVERSE_DEFAULTS.rankMin;
168
+ const max = rank.max ?? UNIVERSE_DEFAULTS.rankMax;
169
+ if (int(min, 1, 10_000) && int(max, 1, 10_000) && min > max)
170
+ add("skill_universe_rank", "universe.rank.min must be <= rank.max");
171
+ }
172
+ }
173
+ if (u.minVolume24hUsd !== undefined &&
174
+ !(typeof u.minVolume24hUsd === "number" &&
175
+ Number.isFinite(u.minVolume24hUsd) &&
176
+ u.minVolume24hUsd >= 0))
177
+ add("skill_universe_volume", "universe.minVolume24hUsd must be a number >= 0");
178
+ if (u.excludeStablecoins !== undefined &&
179
+ typeof u.excludeStablecoins !== "boolean")
180
+ add("skill_universe_stablecoins", "universe.excludeStablecoins must be true or false");
181
+ for (const key of ["includeSectors", "excludeSectors"]) {
182
+ const v = u[key];
183
+ if (v === undefined)
184
+ continue;
185
+ if (!Array.isArray(v) ||
186
+ v.length > 50 ||
187
+ !v.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s)))
188
+ add("skill_universe_sectors", `universe.${key} must be a list of sector ids like "defi" or "eco-solana" (GET /api/agent/universe/sectors)`);
189
+ }
190
+ if (u.sort !== undefined &&
191
+ !UNIVERSE_SORTS.includes(u.sort))
192
+ add("skill_universe_sort", `universe.sort must be one of ${UNIVERSE_SORTS.join(", ")}`);
193
+ if (u.resolveTop !== undefined &&
194
+ !int(u.resolveTop, 1, UNIVERSE_MAX_RESOLVE_TOP))
195
+ add("skill_universe_resolve_top", `universe.resolveTop must be a whole number between 1 and ${UNIVERSE_MAX_RESOLVE_TOP}`);
196
+ if (u.scanLimit !== undefined &&
197
+ !int(u.scanLimit, 1, UNIVERSE_MAX_SCAN_LIMIT))
198
+ add("skill_universe_scan_limit", `universe.scanLimit must be a whole number between 1 and ${UNIVERSE_MAX_SCAN_LIMIT}`);
199
+ return issues;
200
+ }
201
+ /** One line for the prompt: the boundaries in plain words. */
202
+ export function describeUniverse(u) {
203
+ const q = universeQuery(u);
204
+ const parts = [`market-cap rank ${q.rankMin}-${q.rankMax}`];
205
+ if (q.minVolume24hUsd > 0)
206
+ parts.push(`24h volume >= $${Math.round(q.minVolume24hUsd).toLocaleString("en-US")}`);
207
+ if (q.excludeStablecoins)
208
+ parts.push("no stablecoins");
209
+ if (q.includeSectors.length)
210
+ parts.push(`only sectors: ${q.includeSectors.join(", ")}`);
211
+ if (q.excludeSectors.length)
212
+ parts.push(`excluding sectors: ${q.excludeSectors.join(", ")}`);
213
+ parts.push(`ranked by ${q.sort.replace(/_/g, " ")}`);
214
+ return parts.join("; ");
215
+ }
@@ -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.7.0";
8
+ readonly openapiVersion: "1.8.0";
9
9
  readonly mcpPackage: "@coinrithm/mcp-trading";
10
10
  readonly mcpVersion: string;
11
11
  };
@@ -20,7 +20,7 @@ export const COINRITHM_API = {
20
20
  mcpUrl: "https://mcp.coinrithm.com/mcp",
21
21
  // The API CONTRACT version (openapi.yaml info.version). Versioned independently
22
22
  // from the npm package below — hand-bump this when the OpenAPI contract changes.
23
- openapiVersion: "1.7.0",
23
+ openapiVersion: "1.8.0",
24
24
  mcpPackage: "@coinrithm/mcp-trading",
25
25
  mcpVersion: PACKAGE_VERSION,
26
26
  };
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type PmPaperOpen, type PmPaperClose } from "./pmPaperV2.js";
1
2
  export declare const DEFAULT_BASE_URL = "https://api.coinrithm.com";
2
3
  export declare function log(...args: unknown[]): void;
3
4
  export interface ClientConfig {
@@ -110,6 +111,7 @@ export declare class CoinRithmClient {
110
111
  updatedSince?: string;
111
112
  }, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
112
113
  getMarketContext(coinId: string, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
114
+ getSpotReference(coinId: string, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
113
115
  getCandles(coinId: string, query?: {
114
116
  range?: "1H" | "1D" | "1W" | "1M" | "3M";
115
117
  fiat?: string;
@@ -120,6 +122,10 @@ export declare class CoinRithmClient {
120
122
  hours?: number;
121
123
  minImportance?: number;
122
124
  }, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
125
+ discoverPmPaperV2(query?: {
126
+ q?: string;
127
+ limit?: number;
128
+ }, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
123
129
  discoverPmMarkets(query?: {
124
130
  q?: string;
125
131
  source?: "all" | "kalshi" | "polymarket";
@@ -221,6 +227,14 @@ export declare class CoinRithmClient {
221
227
  fraction?: number;
222
228
  idempotencyKey: string;
223
229
  }>, apiKey?: string): Promise<ApiResult>;
230
+ openPmPaperV2(body: PmPaperOpen, apiKey?: string): Promise<ApiResult>;
231
+ replayPmPaperV2Open(body: PmPaperOpen, apiKey?: string): Promise<ApiResult>;
232
+ closePmPaperV2(body: PmPaperClose, apiKey?: string): Promise<ApiResult>;
233
+ pmPaperV2Positions(apiKey?: string, query?: {
234
+ status?: "open";
235
+ beforeId?: number;
236
+ limit?: number;
237
+ }): Promise<ApiResult>;
224
238
  openPmPosition(body: TraceableBody<{
225
239
  source: string;
226
240
  slug: string;
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { PM_PAPER_V2_BASE, } from "./pmPaperV2.js";
1
2
  // Thin HTTP client for the CoinRithm agent surface (/api/agent/*).
2
3
  //
3
4
  // Auth: a user-minted API key (crk_live_…) passed as `Authorization: Bearer …`.
@@ -30,7 +31,7 @@ function resolveBaseUrl() {
30
31
  export function loadConfig() {
31
32
  const apiKey = process.env.COINRITHM_API_KEY?.trim();
32
33
  if (!apiKey) {
33
- throw new Error("COINRITHM_API_KEY is not set. Mint a key in CoinRithm → Profile → API Keys " +
34
+ throw new Error("COINRITHM_API_KEY is not set. Mint a key in CoinRithm → Settings → API Keys " +
34
35
  "and expose it to this MCP server as the COINRITHM_API_KEY environment variable.");
35
36
  }
36
37
  if (!apiKey.startsWith("crk_live_")) {
@@ -339,6 +340,9 @@ export class CoinRithmClient {
339
340
  getMarketContext(coinId, apiKey, agentTrace) {
340
341
  return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey, agentTrace });
341
342
  }
343
+ getSpotReference(coinId, apiKey, agentTrace) {
344
+ return this.request("GET", `/api/agent/spot/reference/${encodeURIComponent(coinId)}`, { apiKey, agentTrace });
345
+ }
342
346
  getCandles(coinId, query, apiKey, agentTrace) {
343
347
  return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}/candles`, { query, apiKey, agentTrace });
344
348
  }
@@ -349,6 +353,13 @@ export class CoinRithmClient {
349
353
  agentTrace,
350
354
  });
351
355
  }
356
+ discoverPmPaperV2(query, apiKey, agentTrace) {
357
+ return this.request("GET", "/api/agent/pm/v2/discover", {
358
+ query,
359
+ apiKey,
360
+ agentTrace,
361
+ });
362
+ }
352
363
  discoverPmMarkets(query, apiKey, agentTrace) {
353
364
  return this.request("GET", "/api/agent/pm/discover", {
354
365
  query,
@@ -435,6 +446,25 @@ export class CoinRithmClient {
435
446
  closeFuturesPosition(body, apiKey) {
436
447
  return this.request("POST", "/api/agent/futures/close", { body, apiKey });
437
448
  }
449
+ // Explicit opt-in contract; API derives and authorizes the configured house tuple.
450
+ openPmPaperV2(body, apiKey) {
451
+ return this.request("POST", `${PM_PAPER_V2_BASE}/open`, { body, apiKey });
452
+ }
453
+ replayPmPaperV2Open(body, apiKey) {
454
+ return this.request("POST", `${PM_PAPER_V2_BASE}/open/replay`, {
455
+ body,
456
+ apiKey,
457
+ });
458
+ }
459
+ closePmPaperV2(body, apiKey) {
460
+ return this.request("POST", `${PM_PAPER_V2_BASE}/close`, { body, apiKey });
461
+ }
462
+ pmPaperV2Positions(apiKey, query) {
463
+ return this.request("GET", `${PM_PAPER_V2_BASE}/positions`, {
464
+ apiKey,
465
+ query,
466
+ });
467
+ }
438
468
  openPmPosition(body, apiKey) {
439
469
  return this.request("POST", "/api/agent/pm/open", { body, apiKey });
440
470
  }
package/dist/http.js CHANGED
@@ -75,7 +75,7 @@ export function createHttpApp(client, options = {}) {
75
75
  res.json({
76
76
  service: "CoinRithm MCP",
77
77
  version: SERVER_VERSION,
78
- description: "Model Context Protocol server for CoinRithm: keyless public prediction-market data tools (pm_data_*) plus paper-trading tools (spot, futures, prediction markets) with a crk_live_ API key. Paper only — never real money.",
78
+ description: "Model Context Protocol server for CoinRithm: keyless prediction-market data tools across 12 venues (pm_data_*), keyless crypto movers and Agent Arena reads, plus crypto market context and paper-trading tools (spot, futures, prediction markets) with a crk_live_ API key. Opt-in PM paper v2 and spot reference tools are off unless the server enables them. Paper only, never real money.",
79
79
  endpoint: {
80
80
  url: "https://mcp.coinrithm.com/mcp",
81
81
  method: "POST",
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ export declare const pmPaperOpenSchema: z.ZodObject<{
3
+ source: z.ZodEnum<["kalshi", "polymarket"]>;
4
+ slug: z.ZodString;
5
+ outcomeExternalMarketId: z.ZodString;
6
+ side: z.ZodEnum<["yes", "no"]>;
7
+ maxCashBudget: z.ZodEffects<z.ZodString, string, string>;
8
+ idempotencyKey: z.ZodString;
9
+ minEntryProbabilityPct: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
10
+ forecastProbability: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
11
+ thesis: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
12
+ }, "strict", z.ZodTypeAny, {
13
+ source: "kalshi" | "polymarket";
14
+ slug: string;
15
+ outcomeExternalMarketId: string;
16
+ side: "yes" | "no";
17
+ maxCashBudget: string;
18
+ idempotencyKey: string;
19
+ minEntryProbabilityPct?: string | null | undefined;
20
+ forecastProbability?: string | null | undefined;
21
+ thesis?: string | null | undefined;
22
+ }, {
23
+ source: "kalshi" | "polymarket";
24
+ slug: string;
25
+ outcomeExternalMarketId: string;
26
+ side: "yes" | "no";
27
+ maxCashBudget: string;
28
+ idempotencyKey: string;
29
+ minEntryProbabilityPct?: string | null | undefined;
30
+ forecastProbability?: string | null | undefined;
31
+ thesis?: string | null | undefined;
32
+ }>;
33
+ export declare const pmPaperCloseSchema: z.ZodObject<{
34
+ positionId: z.ZodNumber;
35
+ idempotencyKey: z.ZodString;
36
+ detail: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
37
+ }, "strict", z.ZodTypeAny, {
38
+ idempotencyKey: string;
39
+ positionId: number;
40
+ detail?: string | null | undefined;
41
+ }, {
42
+ idempotencyKey: string;
43
+ positionId: number;
44
+ detail?: string | null | undefined;
45
+ }>;
46
+ export declare const pmForecastProbabilitySchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
47
+ export declare const pmPaperAgentOpenSchema: z.ZodObject<{
48
+ source: z.ZodEnum<["kalshi", "polymarket"]>;
49
+ slug: z.ZodString;
50
+ outcomeExternalMarketId: z.ZodString;
51
+ side: z.ZodEnum<["yes", "no"]>;
52
+ maxCashBudget: z.ZodEffects<z.ZodString, string, string>;
53
+ idempotencyKey: z.ZodString;
54
+ minEntryProbabilityPct: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
55
+ thesis: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
56
+ } & {
57
+ forecastProbability: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
58
+ }, "strict", z.ZodTypeAny, {
59
+ source: "kalshi" | "polymarket";
60
+ slug: string;
61
+ outcomeExternalMarketId: string;
62
+ side: "yes" | "no";
63
+ maxCashBudget: string;
64
+ idempotencyKey: string;
65
+ forecastProbability: string;
66
+ minEntryProbabilityPct?: string | null | undefined;
67
+ thesis?: string | null | undefined;
68
+ }, {
69
+ source: "kalshi" | "polymarket";
70
+ slug: string;
71
+ outcomeExternalMarketId: string;
72
+ side: "yes" | "no";
73
+ maxCashBudget: string;
74
+ idempotencyKey: string;
75
+ forecastProbability: string;
76
+ minEntryProbabilityPct?: string | null | undefined;
77
+ thesis?: string | null | undefined;
78
+ }>;
79
+ export type PmPaperOpen = z.infer<typeof pmPaperOpenSchema>;
80
+ export type PmPaperClose = z.infer<typeof pmPaperCloseSchema>;
81
+ export declare const PM_PAPER_V2_BASE = "/api/agent/pm/v2";
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ // Exact decimal wire values; never derive a fee-inclusive budget from stakeMusd.
3
+ const decimal = z
4
+ .string()
5
+ .max(40)
6
+ .regex(/^(0|[1-9]\d*)(\.\d{1,18})?$/);
7
+ const boundedDecimal = (min, max, exclusive = false) => decimal.refine((value) => {
8
+ if (!/^(0|[1-9]\d*)(\.\d{1,18})?$/.test(value) || value.length > 40)
9
+ return false;
10
+ const [whole, fraction = ""] = value.split(".");
11
+ const scaled = BigInt(whole) * 10n ** 18n + BigInt(fraction.padEnd(18, "0"));
12
+ const lower = BigInt(min) * 10n ** 18n, upper = BigInt(max) * 10n ** 18n;
13
+ return exclusive
14
+ ? scaled > lower && scaled < upper
15
+ : scaled >= lower && scaled <= upper;
16
+ });
17
+ const text = (max) => z
18
+ .string()
19
+ .max(max)
20
+ .refine((value) => [...value].every((char) => char.charCodeAt(0) >= 32 && char.charCodeAt(0) !== 127));
21
+ export const pmPaperOpenSchema = z
22
+ .object({
23
+ source: z.enum(["kalshi", "polymarket"]),
24
+ slug: z.string().min(1).max(256),
25
+ outcomeExternalMarketId: z.string().min(1).max(256),
26
+ side: z.enum(["yes", "no"]),
27
+ maxCashBudget: boundedDecimal(10, 1000000).describe("Exact decimal string: maximum total paper cash including entry fees; 10..1000000 mUSD."),
28
+ idempotencyKey: z.string().regex(/^pm-entry:[A-Za-z0-9:_-]{1,111}$/),
29
+ minEntryProbabilityPct: boundedDecimal(0, 100).nullable().optional(),
30
+ forecastProbability: boundedDecimal(0, 100, true).nullable().optional(),
31
+ thesis: text(280).nullable().optional(),
32
+ })
33
+ .strict();
34
+ export const pmPaperCloseSchema = z
35
+ .object({
36
+ positionId: z.number().int().positive().max(2147483647),
37
+ idempotencyKey: z.string().regex(/^pm-exit:[A-Za-z0-9:_-]{1,111}$/),
38
+ detail: text(1024).nullable().optional(),
39
+ })
40
+ .strict();
41
+ // Matches backend-v2 normalizePmPaperEntryRequest (acbf1cc): the chance that
42
+ // the BOUGHT side pays out. Below 1 is read as a fraction (x100), above 1 as a
43
+ // percent; exactly 1 is ambiguous and refused; 0 and 100 are refused.
44
+ const exactlyOne = /^1(\.0{1,18})?$/;
45
+ export const pmForecastProbabilitySchema = boundedDecimal(0, 100, true)
46
+ .refine((value) => !exactlyOne.test(value))
47
+ .describe("Required exact decimal string: chance that the side you buy pays out. Percent above 1 and below 100 (e.g. 62.5), or a fraction above 0 and below 1 (0.625 = 62.5%); exactly 1 is refused.");
48
+ // API-key opens: the API answers 422 forecast_required without a forecast.
49
+ // The base schema above stays optional so stored legacy requests can replay.
50
+ export const pmPaperAgentOpenSchema = pmPaperOpenSchema
51
+ .extend({ forecastProbability: pmForecastProbabilitySchema })
52
+ .strict();
53
+ export const PM_PAPER_V2_BASE = "/api/agent/pm/v2";