@coinrithm/mcp-trading 0.7.14 → 0.7.16

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 (72) hide show
  1. package/CHANGELOG.md +811 -613
  2. package/README.md +415 -366
  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/cli.d.ts +25 -0
  20. package/dist/agent/cli.js +300 -2
  21. package/dist/agent/client.d.ts +14 -0
  22. package/dist/agent/client.js +113 -4
  23. package/dist/agent/configurationWarnings.js +11 -0
  24. package/dist/agent/decision.d.ts +3 -0
  25. package/dist/agent/decision.js +85 -3
  26. package/dist/agent/decisionReceipt.d.ts +5 -1
  27. package/dist/agent/decisionReceipt.js +52 -3
  28. package/dist/agent/decisionValidator.d.ts +30 -0
  29. package/dist/agent/decisionValidator.js +147 -2
  30. package/dist/agent/engine.d.ts +3 -3
  31. package/dist/agent/engine.js +3 -3
  32. package/dist/agent/futuresEligibility.d.ts +36 -0
  33. package/dist/agent/futuresEligibility.js +90 -0
  34. package/dist/agent/observe.d.ts +35 -2
  35. package/dist/agent/observe.js +969 -128
  36. package/dist/agent/pmContext.d.ts +10 -1
  37. package/dist/agent/pmContext.js +163 -1
  38. package/dist/agent/prompt.d.ts +18 -1
  39. package/dist/agent/prompt.js +229 -33
  40. package/dist/agent/promptTables.d.ts +1 -0
  41. package/dist/agent/promptTables.js +33 -0
  42. package/dist/agent/providerCapabilities.d.ts +9 -0
  43. package/dist/agent/providerCapabilities.js +57 -4
  44. package/dist/agent/providers.d.ts +28 -7
  45. package/dist/agent/providers.js +200 -50
  46. package/dist/agent/resolve.js +3 -0
  47. package/dist/agent/runner.d.ts +8 -0
  48. package/dist/agent/runner.js +159 -26
  49. package/dist/agent/setups.d.ts +2 -1
  50. package/dist/agent/setups.js +17 -16
  51. package/dist/agent/signals.d.ts +33 -0
  52. package/dist/agent/signals.js +89 -0
  53. package/dist/agent/skill.js +37 -0
  54. package/dist/agent/skillValidator.js +33 -0
  55. package/dist/agent/strictLint.js +29 -0
  56. package/dist/agent/templates.js +30 -30
  57. package/dist/agent/types.d.ts +266 -13
  58. package/dist/agent/types.js +4 -0
  59. package/dist/agent/universe.d.ts +78 -0
  60. package/dist/agent/universe.js +215 -0
  61. package/dist/client.d.ts +9 -2
  62. package/dist/client.js +22 -10
  63. package/dist/http.d.ts +1 -0
  64. package/dist/http.js +17 -2
  65. package/dist/httpCompletion.d.ts +1 -1
  66. package/dist/httpCompletion.js +1 -0
  67. package/dist/index.js +2 -1
  68. package/dist/instructions.d.ts +1 -0
  69. package/dist/instructions.js +16 -0
  70. package/dist/tools.d.ts +1 -0
  71. package/dist/tools.js +150 -66
  72. package/package.json +98 -97
package/dist/agent/cli.js CHANGED
@@ -18,12 +18,17 @@ import { buildManifest, writeManifest } from "./manifest.js";
18
18
  import { parseFrontmatter } from "./frontmatter.js";
19
19
  import { renderFolderOfOne, ejectFiles, PRESET_NAMES, } from "./templates.js";
20
20
  import { COINRITHM_API } from "./version.js";
21
- import { stableStringify, envFlag } from "./util.js";
22
- import { CoinRithmClient } from "./client.js";
21
+ import { stableStringify, envFlag, parseCadenceMs, sleep } from "./util.js";
22
+ import { CoinRithmClient, DEFAULT_BASE_URL } from "./client.js";
23
23
  import { selectProvider } from "./providers.js";
24
24
  import { runLoop } from "./runner.js";
25
25
  import { loadState, saveState } from "./state.js";
26
26
  import { makeRunId } from "./runEvidence.js";
27
+ import { readCorpus, writeCassette } from "./bench/cassette.js";
28
+ import { recordCassette } from "./bench/recordingClient.js";
29
+ import { runBench } from "./bench/bench.js";
30
+ import { buildPmLabels, buildPriceLabels, DEFAULT_LABEL_HORIZON_HOURS, } from "./bench/labelBuilder.js";
31
+ import { parseLabelFile } from "./bench/labels.js";
27
32
  const fail = (lines) => ({ ok: false, code: 1, lines });
28
33
  function issuesResult(issues, header) {
29
34
  return {
@@ -438,8 +443,254 @@ export async function cmdRun(path, opts = {}) {
438
443
  release();
439
444
  }
440
445
  }
446
+ const MAX_RECORD_CYCLES = 10_000;
447
+ // Record bench cassettes (see bench/cassette.ts). Read-only by construction:
448
+ // every cycle runs DRY-RUN through a recording transport that refuses all
449
+ // non-GET requests, with a brain that never calls a model, so only
450
+ // COINRITHM_API_KEY is needed and no model key is read. The agent's own state
451
+ // file is not touched; each recorded cycle starts from a fresh run state.
452
+ export async function cmdRecord(path, opts = {}) {
453
+ if (!opts.out)
454
+ return fail(["record needs --out <dir> for the cassettes"]);
455
+ const cycles = opts.cycles ?? 1;
456
+ if (!Number.isInteger(cycles) || cycles < 1 || cycles > MAX_RECORD_CYCLES)
457
+ return fail([`--cycles must be an integer from 1 to ${MAX_RECORD_CYCLES}`]);
458
+ let loaded;
459
+ try {
460
+ loaded = loadAgent(path, "self-host");
461
+ }
462
+ catch (e) {
463
+ if (e instanceof ResolveError)
464
+ return issuesResult(e.issues, "resolve failed");
465
+ throw e;
466
+ }
467
+ const everyMs = opts.every !== undefined
468
+ ? parseCadenceMs(opts.every)
469
+ : (parseCadenceMs(loaded.spec.trigger.cadence) ?? 3_600_000);
470
+ if (everyMs === null)
471
+ return fail([`--every "${opts.every}" is not a cadence like 5m or 1h`]);
472
+ const apiKey = process.env.COINRITHM_API_KEY;
473
+ if (!apiKey)
474
+ return fail([
475
+ "COINRITHM_API_KEY is not set (recording reads your paper account; it never writes)",
476
+ ]);
477
+ const mergedProse = runtimeProse(loaded.resolved);
478
+ const lines = [
479
+ `record DRY-RUN (reads only, no model call): ${loaded.spec.name}, ${cycles} cycle(s)`,
480
+ ];
481
+ const files = [];
482
+ for (let i = 0; i < cycles; i++) {
483
+ let cassette;
484
+ try {
485
+ cassette = await recordCassette({
486
+ spec: loaded.spec,
487
+ mergedProse,
488
+ apiKey,
489
+ baseUrl: process.env.COINRITHM_API_URL || undefined,
490
+ fetchFn: opts.fetchFn,
491
+ });
492
+ }
493
+ catch (e) {
494
+ lines.push(`✗ cycle ${i + 1} failed: ${e.message}`);
495
+ return { ok: false, code: 1, lines, data: files };
496
+ }
497
+ files.push(writeCassette(resolvePath(opts.out), cassette));
498
+ const cycle = cassette.recordCycle;
499
+ lines.push(`recorded ${cassette.id}: ${cassette.responses.length} read(s), asOf ${cassette.asOf}, cycle ${cycle.decisionType ?? cycle.decision}${cycle.skipReason ? ` (${cycle.skipReason})` : ""}` +
500
+ (cassette.refusedRequests.length
501
+ ? `, ${cassette.refusedRequests.length} write(s) refused`
502
+ : ""));
503
+ if (i < cycles - 1)
504
+ await (opts.sleepFn ?? sleep)(everyMs);
505
+ }
506
+ return { ok: true, code: 0, lines, data: files };
507
+ }
508
+ // Write outcome labels for a recorded corpus (bench/labelBuilder.ts), reads
509
+ // only: price bars from candles published after each cassette's asOf (with
510
+ // COINRITHM_API_KEY), and PM settlement from the public event verdict
511
+ // (settlementEligible + "settle" + a provider won/lost outcome result; any
512
+ // other state stays unlabelled).
513
+ export async function cmdLabel(opts = {}) {
514
+ if (!opts.corpus)
515
+ return fail(["label needs --corpus <dir>"]);
516
+ const horizonHours = opts.horizonHours ?? DEFAULT_LABEL_HORIZON_HOURS;
517
+ if (!Number.isFinite(horizonHours) || horizonHours <= 0 || horizonHours > 720)
518
+ return fail(["--horizon-hours must be a number in (0, 720]"]);
519
+ const apiKey = process.env.COINRITHM_API_KEY;
520
+ if (!apiKey)
521
+ return fail([
522
+ "COINRITHM_API_KEY is not set (labels read public market candles through your key; nothing is written)",
523
+ ]);
524
+ const dir = resolvePath(opts.corpus);
525
+ let corpus;
526
+ try {
527
+ corpus = readCorpus(dir);
528
+ }
529
+ catch (e) {
530
+ return fail([e.message]);
531
+ }
532
+ const client = new CoinRithmClient({
533
+ apiKey,
534
+ baseUrl: process.env.COINRITHM_API_URL || undefined,
535
+ fetchFn: opts.fetchFn,
536
+ });
537
+ const results = await buildPriceLabels(corpus.cassettes, {
538
+ fetchCandles: (coinId, range) => client.candles(coinId, range),
539
+ horizonHours,
540
+ nowMs: opts.nowMs ?? Date.now(),
541
+ existing: corpus.labels,
542
+ overwrite: opts.overwrite,
543
+ });
544
+ const base = (process.env.COINRITHM_API_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
545
+ const fetchFn = opts.fetchFn ?? fetch;
546
+ const pmResults = await buildPmLabels(corpus.cassettes, {
547
+ // The public event detail: the same verdict the event page shows.
548
+ fetchEvent: async (source, slug) => {
549
+ const res = await fetchFn(`${base}/api/prediction-markets/events/${encodeURIComponent(source)}/${encodeURIComponent(slug)}`, { method: "GET", signal: AbortSignal.timeout(15_000) });
550
+ return {
551
+ ok: res.ok,
552
+ status: res.status,
553
+ data: res.ok ? await res.json() : null,
554
+ };
555
+ },
556
+ existing: corpus.labels,
557
+ overwrite: opts.overwrite,
558
+ });
559
+ const lines = [
560
+ `label (reads only): ${corpus.cassettes.length} cassette(s), horizon ${horizonHours} h`,
561
+ ];
562
+ let written = 0;
563
+ for (const r of results) {
564
+ const pm = pmResults.find((p) => p.id === r.id);
565
+ const notes = [];
566
+ let file;
567
+ if (r.status === "built") {
568
+ file = { ...r.file };
569
+ notes.push(`${r.range} bars for ${r.symbols.length} symbol(s)${r.missing.length ? `, missing ${r.missing.join(",")}` : ""}`);
570
+ }
571
+ else {
572
+ notes.push(`prices ${r.status}${r.status === "not_yet" ? ` (labelable after ${r.labelableAfter})` : ""}${r.status === "horizon_mismatch" ? ` (existing file labels ${r.existingHorizonHours ?? "no"} h, not ${horizonHours} h; rerun with --overwrite)` : ""}`);
573
+ }
574
+ if (pm) {
575
+ notes.push(`pm ${pm.labelled} new settled outcome(s) from ${pm.events} event(s)${pm.failed ? `, ${pm.failed} read(s) failed` : ""}`);
576
+ const hasPm = Object.keys(pm.pm).length > 0;
577
+ if (opts.overwrite) {
578
+ // --overwrite REPLACES the PM part, even with nothing: a label that
579
+ // was settled before but is void/unresolved now must not survive.
580
+ const base = file ?? corpus.labels[r.id];
581
+ if (base || hasPm) {
582
+ const { pm: _replaced, ...rest } = base ?? {};
583
+ file = hasPm ? { ...rest, pm: pm.pm } : rest;
584
+ }
585
+ }
586
+ else if (pm.labelled > 0 || (file && hasPm))
587
+ file = { ...(file ?? corpus.labels[r.id] ?? {}), pm: pm.pm };
588
+ }
589
+ lines.push(`${r.id}: ${notes.join("; ")}`);
590
+ if (!file)
591
+ continue;
592
+ const path = join(dir, "labels", `${r.id}.json`);
593
+ // The bench reads it back through the same validator; fail closed here.
594
+ parseLabelFile(file, `labels/${r.id}.json`);
595
+ mkdirSync(dirname(path), { recursive: true });
596
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`, "utf8");
597
+ written += 1;
598
+ }
599
+ lines.push(`wrote ${written} label file(s); PM outcomes without a settled platform verdict stay unlabelled`);
600
+ return { ok: true, code: 0, lines, data: { prices: results, pm: pmResults } };
601
+ }
602
+ // Compare agent variants on a recorded corpus (see bench/bench.ts). Each
603
+ // variant runs with its own model, built exactly as `run` builds it from the
604
+ // variant's model config and the environment's keys. Never writes.
605
+ export async function cmdBench(opts = {}) {
606
+ if (!opts.corpus)
607
+ return fail(["bench needs --corpus <dir>"]);
608
+ if (!opts.variants || opts.variants.length === 0)
609
+ return fail(["bench needs at least one --variant name=<agentPath>"]);
610
+ if (opts.seed !== undefined &&
611
+ (!Number.isSafeInteger(opts.seed) || opts.seed < 0))
612
+ return fail(["--seed must be a non-negative integer"]);
613
+ let corpus;
614
+ try {
615
+ corpus = readCorpus(resolvePath(opts.corpus));
616
+ }
617
+ catch (e) {
618
+ return fail([e.message]);
619
+ }
620
+ const variants = [];
621
+ for (const arg of opts.variants) {
622
+ const eq = arg.indexOf("=");
623
+ if (eq <= 0 || eq === arg.length - 1)
624
+ return fail([`--variant "${arg}" must look like name=<agentPath>`]);
625
+ const name = arg.slice(0, eq);
626
+ const agentPath = arg.slice(eq + 1);
627
+ let loaded;
628
+ try {
629
+ loaded = loadAgent(agentPath, "self-host");
630
+ }
631
+ catch (e) {
632
+ if (e instanceof ResolveError)
633
+ return issuesResult(e.issues, `variant ${name}: resolve failed`);
634
+ throw e;
635
+ }
636
+ let provider;
637
+ try {
638
+ provider = opts.providerFor
639
+ ? opts.providerFor(loaded.spec)
640
+ : selectProvider(loaded.spec, process.env, fetch);
641
+ }
642
+ catch (e) {
643
+ return fail([`variant ${name}: ${e.message}`]);
644
+ }
645
+ variants.push({
646
+ name,
647
+ spec: loaded.spec,
648
+ mergedProse: runtimeProse(loaded.resolved),
649
+ provider,
650
+ });
651
+ }
652
+ const lines = [
653
+ `bench DRY-RUN: ${corpus.cassettes.length} cassette(s) x ${variants.length} variant(s) x ${opts.repeats ?? 3} repeat(s)`,
654
+ ];
655
+ if (corpus.ignored.length)
656
+ lines.push(`ignored non-cassette file(s): ${corpus.ignored.join(", ")}`);
657
+ let report;
658
+ try {
659
+ report = await runBench({
660
+ cassettes: corpus.cassettes,
661
+ labels: corpus.labels,
662
+ variants,
663
+ repeats: opts.repeats,
664
+ seed: opts.seed,
665
+ baselines: opts.baselines,
666
+ });
667
+ }
668
+ catch (e) {
669
+ return fail([...lines, `✗ ${e.message}`]);
670
+ }
671
+ const summary = report.variants;
672
+ for (const [name, v] of Object.entries(summary)) {
673
+ const codes = Object.entries(v.all.actions.rejectCodes)
674
+ .map(([c, n]) => `${c} ${n}`)
675
+ .join(", ");
676
+ lines.push(`${name}: ${v.all.cycles} cycle(s), accepted ${v.all.actions.accepted}, rejected ${v.all.actions.rejected}${codes ? ` (${codes})` : ""}, model failures ${v.all.modelFailures}, cycles missing inputs ${v.all.cyclesWithMissingInputs}, repeat consistency ${v.all.repeatConsistency ?? "n/a"}`);
677
+ }
678
+ for (const c of report.comparisons) {
679
+ const accepted = c.all.metrics.acceptedActions;
680
+ lines.push(`${c.a} vs ${c.b}: overlap ${c.all.actionOverlapJaccard ?? "n/a"}, accepted diff ${accepted.meanDiff ?? "n/a"} (95% CI ${accepted.ci95 ? `${accepted.ci95[0]}..${accepted.ci95[1]}` : "n/a"}, n=${accepted.n})`);
681
+ }
682
+ if (opts.out) {
683
+ writeFileSync(resolvePath(opts.out), `${JSON.stringify(report, null, 2)}\n`, "utf8");
684
+ lines.push(`wrote ${resolvePath(opts.out)}`);
685
+ }
686
+ lines.push(`contentHash ${report.contentHash}`);
687
+ return { ok: true, code: 0, lines, data: report };
688
+ }
441
689
  function parseFlags(args) {
442
690
  const out = { _: [] };
691
+ // A missing or non-numeric value becomes NaN so the command rejects it,
692
+ // never silently falls back to its default.
693
+ const num = (v) => (v === undefined ? NaN : Number(v));
443
694
  for (let i = 0; i < args.length; i++) {
444
695
  const a = args[i];
445
696
  if (a === "--hosted")
@@ -462,6 +713,26 @@ function parseFlags(args) {
462
713
  out.state = args[++i];
463
714
  else if (a === "--expect-definition")
464
715
  out.expectDefinition = args[++i] ?? "";
716
+ else if (a === "--out")
717
+ out.out = args[++i];
718
+ else if (a === "--cycles")
719
+ out.cycles = num(args[++i]);
720
+ else if (a === "--every")
721
+ out.every = args[++i] ?? "";
722
+ else if (a === "--corpus")
723
+ out.corpus = args[++i];
724
+ else if (a === "--variant")
725
+ out.variant = [...(out.variant ?? []), args[++i] ?? ""];
726
+ else if (a === "--repeats")
727
+ out.repeats = num(args[++i]);
728
+ else if (a === "--seed")
729
+ out.seed = num(args[++i]);
730
+ else if (a === "--no-baselines")
731
+ out.noBaselines = true;
732
+ else if (a === "--horizon-hours")
733
+ out.horizonHours = num(args[++i]);
734
+ else if (a === "--overwrite")
735
+ out.overwrite = true;
465
736
  else
466
737
  out._.push(a);
467
738
  }
@@ -476,6 +747,9 @@ function usageLines() {
476
747
  " eject <agent.md | dir>",
477
748
  " lock <path>",
478
749
  " run <path> [--once] [--live] [--dry-run] [--state <file>] [--expect-definition sha256:...] (dry-run by default)",
750
+ " record <path> --out <dir> [--cycles N] [--every 5m] (bench inputs; reads only, no model call)",
751
+ " bench --corpus <dir> --variant a=<path> --variant b=<path> [--repeats 3] [--seed N] [--no-baselines] [--out report.json]",
752
+ " label --corpus <dir> [--horizon-hours 24] [--overwrite] (price outcome labels after asOf; reads only)",
479
753
  ];
480
754
  }
481
755
  export async function main(argv) {
@@ -513,6 +787,30 @@ export async function main(argv) {
513
787
  });
514
788
  break;
515
789
  }
790
+ case "record":
791
+ r = await cmdRecord(pos[0] ?? ".", {
792
+ out: flags.out,
793
+ cycles: flags.cycles,
794
+ every: flags.every,
795
+ });
796
+ break;
797
+ case "label":
798
+ r = await cmdLabel({
799
+ corpus: flags.corpus,
800
+ horizonHours: flags.horizonHours,
801
+ overwrite: flags.overwrite,
802
+ });
803
+ break;
804
+ case "bench":
805
+ r = await cmdBench({
806
+ corpus: flags.corpus,
807
+ variants: flags.variant,
808
+ repeats: flags.repeats,
809
+ seed: flags.seed,
810
+ out: flags.out,
811
+ baselines: !flags.noBaselines,
812
+ });
813
+ break;
516
814
  case undefined:
517
815
  case "help":
518
816
  case "--help":
@@ -40,13 +40,27 @@ export declare class CoinRithmClient {
40
40
  rateLimitHits: number;
41
41
  constructor(cfg: ClientConfig);
42
42
  private request;
43
+ private publicRequest;
44
+ credentialFingerprint(): string;
43
45
  me(trace?: AgentTrace): Promise<ApiResult>;
46
+ performance(trace?: AgentTrace, options?: {
47
+ timeoutMs?: number;
48
+ maxRetries?: number;
49
+ }): Promise<ApiResult>;
44
50
  portfolio(trace?: AgentTrace): Promise<ApiResult>;
45
51
  wallet(query?: {
46
52
  coinId?: string;
47
53
  }, trace?: AgentTrace): Promise<ApiResult>;
48
54
  resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
49
55
  cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
56
+ agentUniverse(query: Record<string, string | number>, trace?: AgentTrace): Promise<ApiResult>;
57
+ getPublicPmWhales(options?: {
58
+ timeoutMs?: number;
59
+ limit?: number;
60
+ }): Promise<ApiResult>;
61
+ getPublicPmWhaleWallet(source: string, wallet: string, options?: {
62
+ timeoutMs?: number;
63
+ }): Promise<ApiResult>;
50
64
  market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
51
65
  candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
52
66
  trades(query?: {
@@ -4,6 +4,7 @@
4
4
  // from an agent file). 429 backs off on Retry-After; 401/403/409/422 are
5
5
  // FAIL-CLOSED cycle outcomes (returned, not retried). fetch + sleep are
6
6
  // injectable so tests run with no network and no real waits.
7
+ import { createHash } from "node:crypto";
7
8
  import { setTimeout as sleep } from "node:timers/promises";
8
9
  import { retryAfterSeconds } from "../retryAfter.js";
9
10
  export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
@@ -54,6 +55,16 @@ export class CoinRithmClient {
54
55
  this.extraHeaders = cfg.extraHeaders;
55
56
  }
56
57
  async request(method, path, opts = {}) {
58
+ const requestTimeoutMs = opts.timeoutMs !== undefined &&
59
+ Number.isSafeInteger(opts.timeoutMs) &&
60
+ opts.timeoutMs >= 1
61
+ ? Math.min(opts.timeoutMs, this.requestTimeoutMs)
62
+ : this.requestTimeoutMs;
63
+ const maxRetries = opts.maxRetries !== undefined &&
64
+ Number.isSafeInteger(opts.maxRetries) &&
65
+ opts.maxRetries >= 0
66
+ ? Math.min(opts.maxRetries, this.maxRetries)
67
+ : this.maxRetries;
57
68
  const url = new URL(this.baseUrl + path);
58
69
  if (opts.query) {
59
70
  for (const [k, v] of Object.entries(opts.query)) {
@@ -76,7 +87,7 @@ export class CoinRithmClient {
76
87
  const timer = setTimeout(() => {
77
88
  timedOut = true;
78
89
  controller.abort(new Error("API request deadline exceeded"));
79
- }, this.requestTimeoutMs);
90
+ }, requestTimeoutMs);
80
91
  this.signal?.addEventListener("abort", cancel, { once: true });
81
92
  if (this.signal?.aborted)
82
93
  cancel();
@@ -102,12 +113,12 @@ export class CoinRithmClient {
102
113
  const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
103
114
  if (res.status === 429)
104
115
  this.rateLimitHits += 1;
105
- if (res.status === 429 && attempt < this.maxRetries) {
116
+ if (res.status === 429 && attempt < maxRetries) {
106
117
  // Release this response before waiting so retries do not retain sockets.
107
118
  void res.body?.cancel().catch(() => { });
108
119
  const delayMs = (retryAfter ?? 5) * 1000;
109
120
  // Never shorten a provider's Retry-After or overflow a Node timer.
110
- if (delayMs >= this.requestTimeoutMs)
121
+ if (delayMs >= requestTimeoutMs)
111
122
  await aborted;
112
123
  else if (this.sleepFn)
113
124
  await this.sleepFn(delayMs);
@@ -151,7 +162,7 @@ export class CoinRithmClient {
151
162
  ? "request_aborted"
152
163
  : "network_error",
153
164
  message: timedOut
154
- ? `API request exceeded ${this.requestTimeoutMs}ms deadline`
165
+ ? `API request exceeded ${requestTimeoutMs}ms deadline`
155
166
  : controller.signal.aborted
156
167
  ? "API request cancelled"
157
168
  : err instanceof Error
@@ -166,10 +177,96 @@ export class CoinRithmClient {
166
177
  controller.signal.removeEventListener("abort", rejectAborted);
167
178
  }
168
179
  }
180
+ // Keyless, bounded reads against the public PM surface. These deliberately
181
+ // bypass the authenticated request path so a runner key cannot leak into a
182
+ // public context lookup. No retries are added here; the enrichment is
183
+ // optional and its deadline includes response-body consumption.
184
+ async publicRequest(path, query, timeoutMs = 5_000) {
185
+ const url = new URL(this.baseUrl + path);
186
+ for (const [key, value] of Object.entries(query ?? {})) {
187
+ if (value !== undefined && value !== null && value !== "")
188
+ url.searchParams.set(key, String(value));
189
+ }
190
+ const controller = new AbortController();
191
+ if (this.signal?.aborted) {
192
+ return { ok: false, status: 0, data: { error: "network_error" } };
193
+ }
194
+ const cancel = () => controller.abort();
195
+ this.signal?.addEventListener("abort", cancel, { once: true });
196
+ const deadlineMs = Math.min(timeoutMs, this.requestTimeoutMs);
197
+ let timedOut = false;
198
+ const timer = setTimeout(() => {
199
+ timedOut = true;
200
+ controller.abort();
201
+ }, deadlineMs);
202
+ let rejectAborted;
203
+ const aborted = new Promise((_, reject) => {
204
+ rejectAborted = () => reject(new Error("public request aborted"));
205
+ controller.signal.addEventListener("abort", rejectAborted, {
206
+ once: true,
207
+ });
208
+ });
209
+ if (this.signal?.aborted)
210
+ cancel();
211
+ try {
212
+ const perform = async () => {
213
+ const res = await this.fetchFn(url.toString(), {
214
+ method: "GET",
215
+ headers: { Accept: "application/json" },
216
+ signal: controller.signal,
217
+ });
218
+ const text = await res.text();
219
+ let data = text;
220
+ if (text) {
221
+ try {
222
+ data = JSON.parse(text);
223
+ }
224
+ catch {
225
+ /* leave as text */
226
+ }
227
+ }
228
+ return { ok: res.ok, status: res.status, data };
229
+ };
230
+ return await Promise.race([perform(), aborted]);
231
+ }
232
+ catch {
233
+ return {
234
+ ok: false,
235
+ status: 0,
236
+ data: {
237
+ error: timedOut ? "timeout" : "network_error",
238
+ },
239
+ };
240
+ }
241
+ finally {
242
+ clearTimeout(timer);
243
+ controller.signal.removeEventListener("abort", rejectAborted);
244
+ this.signal?.removeEventListener("abort", cancel);
245
+ }
246
+ }
247
+ // A stable, non-reversible identity for this credential and API base, for
248
+ // per-agent in-memory caches that must outlive one client instance (the
249
+ // hosted scheduler builds a new client every cycle). Never the key itself.
250
+ credentialFingerprint() {
251
+ return createHash("sha256")
252
+ .update(`${this.baseUrl}\n${this.apiKey}`, "utf8")
253
+ .digest("hex")
254
+ .slice(0, 32);
255
+ }
169
256
  // ── reads ──────────────────────────────────────────────────────────────────
170
257
  me(trace) {
171
258
  return this.request("GET", "/api/agent/me", { trace });
172
259
  }
260
+ // GET /api/agent/performance: the calling key's own realized performance.
261
+ // Newer backends add an optional `pmCalibration` block (the key's settled PM
262
+ // forecast record), parsed defensively in observe.
263
+ performance(trace, options) {
264
+ return this.request("GET", "/api/agent/performance", {
265
+ trace,
266
+ timeoutMs: options?.timeoutMs,
267
+ maxRetries: options?.maxRetries,
268
+ });
269
+ }
173
270
  portfolio(trace) {
174
271
  return this.request("GET", "/api/agent/portfolio", { trace });
175
272
  }
@@ -186,6 +283,18 @@ export class CoinRithmClient {
186
283
  ? "/api/coins/top-losers"
187
284
  : "/api/coins/top-gainers", { query: { limit }, trace });
188
285
  }
286
+ // Market-boundaries screener (GET /api/agent/universe, read scope): rows
287
+ // inside the agent's declared rank band / volume floor / sectors, sorted by
288
+ // the agent's notion of an opportunity. Shared 60 s server cache.
289
+ agentUniverse(query, trace) {
290
+ return this.request("GET", "/api/agent/universe", { query, trace });
291
+ }
292
+ getPublicPmWhales(options) {
293
+ return this.publicRequest("/api/prediction-markets/whales", { limit: options?.limit }, options?.timeoutMs);
294
+ }
295
+ getPublicPmWhaleWallet(source, wallet, options) {
296
+ return this.publicRequest(`/api/prediction-markets/whales/wallets/${encodeURIComponent(source)}/${encodeURIComponent(wallet)}`, undefined, options?.timeoutMs);
297
+ }
189
298
  market(coinId, trace) {
190
299
  return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
191
300
  }
@@ -1,6 +1,17 @@
1
+ // Per-day counters roll over on the UTC date (util.ts dayKey), whatever the
2
+ // bundle declares. trigger.timezone is parsed but read by nothing else, so a
3
+ // non-UTC value would silently promise local-day limits.
4
+ const UTC_NAMES = new Set(["utc", "etc/utc", "z", "gmt", "etc/gmt"]);
1
5
  // Advisory only: these fields remain loadable for backwards compatibility.
2
6
  export function configurationWarnings(raw) {
3
7
  const warnings = [];
8
+ const trigger = raw.trigger;
9
+ if (trigger && typeof trigger === "object" && !Array.isArray(trigger)) {
10
+ const tz = trigger.timezone;
11
+ if (typeof tz === "string" && !UTC_NAMES.has(tz.trim().toLowerCase())) {
12
+ warnings.push(`trigger.timezone "${tz}" is informational: maxTradesPerDay and maxDailyLossMusd reset at 00:00 UTC, not in this timezone`);
13
+ }
14
+ }
4
15
  if (Array.isArray(raw.capabilities) &&
5
16
  raw.capabilities.includes("websearch")) {
6
17
  warnings.push("capabilities.websearch is reserved: the runner does not perform web searches");
@@ -537,11 +537,14 @@ export declare const DECISION_JSON_SCHEMA: {
537
537
  };
538
538
  }];
539
539
  };
540
+ export declare const ACTIONS_STRING_DIAGNOSTICS: readonly ["json_array_empty_valid_decision", "json_array_nonempty_valid_decision", "json_array_invalid_decision", "json_nonarray", "invalid_json", "empty_string_valid_skip", "empty_string_other_decision", "non_json_array_text", "json5_array_valid_decision", "json5_array_invalid_decision", "over_size_limit"];
541
+ export type ActionsStringDiagnostic = (typeof ACTIONS_STRING_DIAGNOSTICS)[number];
540
542
  export type ParseDecisionResult = {
541
543
  ok: true;
542
544
  decision: Decision;
543
545
  } | {
544
546
  ok: false;
545
547
  error: string;
548
+ actionsStringDiagnostic?: ActionsStringDiagnostic;
546
549
  };
547
550
  export declare function parseDecision(text: string): ParseDecisionResult;