paralith-mcp 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -125,7 +125,6 @@ function walletShim(jwk) {
125
125
  var enc, sha384, sha256, b64url, b64urlDec, jwkToAddress;
126
126
  var init_ans104 = __esm({
127
127
  "../../backend/native/ans104.mjs"() {
128
- "use strict";
129
128
  enc = new TextEncoder();
130
129
  sha384 = (b) => crypto.createHash("sha384").update(b).digest();
131
130
  sha256 = (b) => crypto.createHash("sha256").update(b).digest();
@@ -196,7 +195,6 @@ async function deriveWallets(phrase, env = process.env) {
196
195
  var HOME, publicAddresses;
197
196
  var init_keys = __esm({
198
197
  "src/keys.mjs"() {
199
- "use strict";
200
198
  init_ans104();
201
199
  HOME = process.env.PARALITH_MCP_HOME || path.join(os.homedir(), ".paralith", "mcp");
202
200
  publicAddresses = (w) => ({
@@ -229,7 +227,6 @@ function saveAgent(name, entry) {
229
227
  var FILE;
230
228
  var init_agents = __esm({
231
229
  "src/agents.mjs"() {
232
- "use strict";
233
230
  init_keys();
234
231
  FILE = path2.join(HOME, "agents.json");
235
232
  }
@@ -337,7 +334,6 @@ Phrases: ${slash(path3.join(HOME, "agents"))} (back them up; anything funded is
337
334
  var WIN, NPX, LAUNCH, slash, installed, CLIENTS;
338
335
  var init_setup = __esm({
339
336
  "src/setup.mjs"() {
340
- "use strict";
341
337
  init_keys();
342
338
  init_agents();
343
339
  WIN = process.platform === "win32";
@@ -485,7 +481,6 @@ async function until(what, read, done, timeoutMs = 6e5, everyMs = 3e3) {
485
481
  var HERE, DIST, graph, NODE, chains, PROCESS_ALIASES, ID, READ_ONLY, loaded, HTML, sleep;
486
482
  var init_runtime = __esm({
487
483
  "src/runtime.mjs"() {
488
- "use strict";
489
484
  init_ans104();
490
485
  init_keys();
491
486
  init_agents();
@@ -511,6 +506,65 @@ var init_runtime = __esm({
511
506
  }
512
507
  });
513
508
 
509
+ // package.json
510
+ var package_default;
511
+ var init_package = __esm({
512
+ "package.json"() {
513
+ package_default = {
514
+ name: "paralith-mcp",
515
+ version: "0.4.6",
516
+ description: "MCP server for Paralith on AO: an AI with one 24-word phrase plays Rune Realm, trades and bridges PUSD; plus paralith-bridge, the bridge operator CLI (TEST deployment).",
517
+ license: "UNLICENSED",
518
+ homepage: "https://docs.paralithmarkets.com/ai/mcp",
519
+ keywords: [
520
+ "mcp",
521
+ "model-context-protocol",
522
+ "ao",
523
+ "hyperbeam",
524
+ "arweave",
525
+ "game",
526
+ "agent"
527
+ ],
528
+ type: "module",
529
+ bin: {
530
+ "paralith-mcp": "dist/server.mjs",
531
+ "paralith-bridge": "dist/bridge.mjs"
532
+ },
533
+ files: [
534
+ "dist",
535
+ "README.md"
536
+ ],
537
+ publishConfig: {
538
+ access: "public"
539
+ },
540
+ scripts: {
541
+ build: "node scripts/build-client.mjs && node scripts/tools-doc.mjs",
542
+ "tools-doc": "node scripts/tools-doc.mjs",
543
+ start: "node src/server.mjs",
544
+ "start:http": "node src/http.mjs",
545
+ test: "node --test test/*.test.mjs"
546
+ },
547
+ engines: {
548
+ node: ">=20"
549
+ },
550
+ dependencies: {
551
+ "@modelcontextprotocol/ext-apps": "^2.0.3",
552
+ "@modelcontextprotocol/sdk": "^1.32.1",
553
+ "@mysten/sui": "^2.34.0",
554
+ "@scure/bip39": "^2.4.0",
555
+ "@solana/web3.js": "^1.99.0",
556
+ "arweave-mnemonic-keys": "^0.0.9",
557
+ lodash: "^4.18.1",
558
+ viem: "^2.57.3",
559
+ zod: "^4.6.5"
560
+ },
561
+ devDependencies: {
562
+ esbuild: "^0.28.2"
563
+ }
564
+ };
565
+ }
566
+ });
567
+
514
568
  // src/update.mjs
515
569
  function check(now) {
516
570
  checkedAt = now;
@@ -528,8 +582,8 @@ function updateNote(now = Date.now()) {
528
582
  var VERSION, CHECK_MS, NOTE_MS, latest, checkedAt, notedAt, newer;
529
583
  var init_update = __esm({
530
584
  "src/update.mjs"() {
531
- "use strict";
532
- VERSION = "0.4.2";
585
+ init_package();
586
+ VERSION = package_default.version;
533
587
  CHECK_MS = 6 * 36e5;
534
588
  NOTE_MS = 36e5;
535
589
  latest = null;
@@ -588,7 +642,6 @@ The message WAS accepted (slot ${error?.slot ?? "?"}); its result could not be c
588
642
  var migrationSeen, bigints, READ, OWN, WRITE;
589
643
  var init_tool = __esm({
590
644
  "src/tool.mjs"() {
591
- "use strict";
592
645
  init_runtime();
593
646
  init_update();
594
647
  migrationSeen = null;
@@ -658,13 +711,20 @@ function side(s) {
658
711
  freeMovesUsed: Object.keys(s.freeUsed ?? {})
659
712
  };
660
713
  }
714
+ function sideOf(b, address) {
715
+ if (!address) return null;
716
+ if ((b.accepterAddress ?? b.accepter?.address) === address) return "accepter";
717
+ if ((b.challengerAddress ?? b.challenger?.address) === address) return "challenger";
718
+ return null;
719
+ }
661
720
  function battle(b, address) {
662
721
  if (!b || typeof b !== "object") return b;
663
- const mine = b.accepterAddress === address ? "accepter" : "challenger";
722
+ const mine = sideOf(b, address);
664
723
  const theirs = mine === "accepter" ? "challenger" : "accepter";
724
+ const label = (s) => !mine ? s : s === mine ? "you" : "opponent";
665
725
  const turns = (b.turns ?? []).filter((t) => t.round === b.round - 1 || t.round === b.round).map((t) => ({
666
726
  round: t.round,
667
- by: t.attacker === mine ? "you" : "opponent",
727
+ by: label(t.attacker),
668
728
  move: t.move,
669
729
  damage: t.healthDamage,
670
730
  shieldDamage: t.shieldDamage,
@@ -673,6 +733,7 @@ function battle(b, address) {
673
733
  superEffective: t.superEffective,
674
734
  notEffective: t.notEffective
675
735
  }));
736
+ const sides = mine ? { you: side(b[mine]), opponent: side(b[theirs]) } : { challenger: side(b.challenger), accepter: side(b.accepter) };
676
737
  return {
677
738
  id: b.id,
678
739
  kind: b.kind,
@@ -680,8 +741,7 @@ function battle(b, address) {
680
741
  round: b.round,
681
742
  ...b.winner ? { winner: b.winner } : {},
682
743
  ...b.waitingOn?.length ? { waitingOn: b.waitingOn } : {},
683
- you: side(b[mine]),
684
- opponent: side(b[theirs]),
744
+ ...sides,
685
745
  recentTurns: turns
686
746
  };
687
747
  }
@@ -697,7 +757,6 @@ function hunt(run2) {
697
757
  var iso, uses, EXTRAS;
698
758
  var init_views = __esm({
699
759
  "src/views.mjs"() {
700
- "use strict";
701
760
  iso = (ms) => ms ? new Date(Number(ms)).toISOString() : void 0;
702
761
  uses = (moves) => Object.fromEntries(Object.entries(moves ?? {}).map(([n, m]) => [n, m?.count ?? m]));
703
762
  EXTRAS = ["rewards", "lootResult", "dailyClaimed", "economyResult", "withdrawal", "arenaLast", "lastHunt", "listing", "error"];
@@ -710,18 +769,25 @@ async function me() {
710
769
  const { api, wallets } = await runtime();
711
770
  return { api, address: wallets?.ao.address };
712
771
  }
713
- async function player2() {
714
- const { api, address } = await me();
715
- if (!address) throw new Error("This server has no wallet; pass an address or battle_id.");
716
- const p = await api.readPlayer(address);
717
- if (!p) throw new Error("This wallet has no Rune Realm account yet. Call game_join first.");
772
+ async function player2(address) {
773
+ const { api, address: mine } = await me();
774
+ const who = address ?? mine;
775
+ if (!who) throw new Error("This server has no wallet; pass an address.");
776
+ const p = await api.readPlayer(who);
777
+ if (!p) throw new Error(address ? `${address} has no Rune Realm account.` : "This wallet has no Rune Realm account yet. Call game_join first.");
718
778
  return p;
719
779
  }
780
+ async function findBattle(id) {
781
+ const fleet = await readKey("game", "battlefleet").catch(() => null);
782
+ const workers = (fleet?.workers ?? []).map((w) => w.workerProcessId).filter(Boolean);
783
+ const found = await Promise.all([readKey("game", `battle-op-${id}`), ...workers.map((w) => readKey(w, `battle-${id}`))].map((read) => read.catch(() => null)));
784
+ return found.find((b) => b && typeof b === "object") ?? null;
785
+ }
720
786
  function registerGame(server) {
721
787
  tool(
722
788
  server,
723
789
  "game_player",
724
- "Read a Rune Realm player record (yours by default): gold, runes, inventory, companions with energy/happiness/level/moves, pending move offers, loot boxes, and any live arena (battleFleet) or hunt route. Free, unsigned. Call this before deciding anything.",
790
+ "Read a Rune Realm player record (yours by default): gold, inventory, companions with energy/happiness/level/moves and pendingMoveOffer, loot boxes, arena record with arena.liveBattle, and liveHunt. Free, unsigned. Call this before deciding anything.",
725
791
  { address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
726
792
  async ({ address }) => {
727
793
  const { api, address: mine } = await me();
@@ -737,7 +803,13 @@ function registerGame(server) {
737
803
  "game_catalog",
738
804
  "The game rules as data: activity costs/gains, arena terms, element effectiveness, free moves (Rally, Mend), hunt capture odds, items, level-up rules and every move pool. Free, unsigned.",
739
805
  {},
740
- async () => (await me()).api.readCatalog(),
806
+ async () => {
807
+ const catalog = await (await me()).api.readCatalog();
808
+ if (catalog?.levelUp) {
809
+ catalog.levelUp = { ...catalog.levelUp, costDivisor: 32, cost: "ceil(targetLevel^2 / 32) Rune" };
810
+ }
811
+ return catalog;
812
+ },
741
813
  READ
742
814
  );
743
815
  tool(
@@ -759,7 +831,11 @@ function registerGame(server) {
759
831
  factions: factions?.map(({ members, ...f }) => f),
760
832
  arenaTiers: tiers,
761
833
  openChallenges: challenges,
762
- leaderboard: leaderboard?.slice(0, 10)
834
+ leaderboard: leaderboard?.slice(0, 10).map(({ monster, ...row }) => {
835
+ if (!monster) return row;
836
+ const { exp, energy, happiness, ...companion2 } = companion(monster);
837
+ return { ...row, companion: companion2 };
838
+ })
763
839
  };
764
840
  },
765
841
  READ
@@ -776,7 +852,7 @@ function registerGame(server) {
776
852
  tool(
777
853
  server,
778
854
  "game_companion",
779
- "Care for and manage companions. feed: 1 berry, +10 energy. play: 10 energy + 1 berry, +25 happiness after 15 min. quest: 25 energy + 25 happiness, 1 h, then exp and Gold. claim: collect a finished play/quest. store (1 Rune) / retrieve / set_active / transfer manage the collection. adopt: once, if you have a faction but no companion.",
855
+ "Care for and manage companions. feed: 1 berry, +20 energy for a berry of its own element, +10 for any other. play: 10 energy + 1 berry, +25 happiness after 15 min. quest: 25 energy + 25 happiness, 1 h, then exp and Gold. claim: collect a finished play/quest. store (1 Rune; moves a companion into the collection) / retrieve / set_active / transfer manage the collection. adopt: once, if you have a faction but no companion.",
780
856
  {
781
857
  action: z2.enum(["feed", "play", "quest", "claim", "adopt", "store", "retrieve", "set_active", "transfer"]),
782
858
  monster_id: z2.string().optional().describe("Companion id; omit for the active one."),
@@ -812,7 +888,7 @@ function registerGame(server) {
812
888
  tool(
813
889
  server,
814
890
  "game_level_up",
815
- "Level up the active companion when it has enough exp. Costs Rune. Allocate exactly the points the catalog says (levelUp.points), at most levelUp.maxPerStat in any one stat.",
891
+ "Level up the active companion when it has enough exp. Costs ceil(L^2/32) in-game Rune for target level L (1 up to level 5, 4 at 10, 13 at 20). Allocate exactly the points the catalog says (levelUp.points), at most levelUp.maxPerStat in any one stat. A pending move offer expires at the next level-up.",
816
892
  { attack: z2.number().int().min(0), defense: z2.number().int().min(0), speed: z2.number().int().min(0), health: z2.number().int().min(0) },
817
893
  async (points) => (await me()).api.levelUp(points),
818
894
  WRITE,
@@ -821,7 +897,7 @@ function registerGame(server) {
821
897
  tool(
822
898
  server,
823
899
  "game_learn_move",
824
- "Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way.",
900
+ "Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way. An unanswered offer expires at the next level-up.",
825
901
  { move: z2.string().describe("The offered move, exactly as in the player record."), replace: z2.string().optional() },
826
902
  async ({ move, replace }) => (await me()).api.learnMove(move, replace),
827
903
  WRITE,
@@ -845,27 +921,31 @@ function registerGame(server) {
845
921
  WRITE,
846
922
  asPlayer
847
923
  );
924
+ tool(
925
+ server,
926
+ "game_shop_quote",
927
+ "The NPC realm shop's current Gold prices (bid/ask) and stock for berries, Scrolls and Rune. Free, unsigned.",
928
+ {},
929
+ async () => (await me()).api.readEconomy(),
930
+ READ
931
+ );
848
932
  tool(
849
933
  server,
850
934
  "game_shop",
851
- "Trade with the NPC realm shop for Gold. quote returns current prices and stock (free). buy/sell trades; set max_total/min_total to bound slippage.",
935
+ "Buy from or sell to the NPC realm shop for Gold (prices: game_shop_quote). Set max_total/min_total to bound slippage.",
852
936
  {
853
- action: z2.enum(["quote", "buy", "sell"]),
854
- item: z2.string().optional().describe("Item id, e.g. fire_berry or scroll."),
855
- quantity: z2.number().int().positive().optional(),
937
+ action: z2.enum(["buy", "sell"]),
938
+ item: z2.enum(SHOP_ITEMS),
939
+ quantity: z2.number().int().positive(),
856
940
  max_total: z2.number().int().optional(),
857
941
  min_total: z2.number().int().optional()
858
942
  },
859
- async ({ action, item, quantity, max_total, min_total }) => {
860
- const { api } = await me();
861
- if (action === "quote") return api.readEconomy();
862
- return api.tradeGameShop(
863
- action,
864
- need(item, "item"),
865
- need(quantity, "quantity"),
866
- { maxTotal: max_total, minTotal: min_total }
867
- );
868
- },
943
+ async ({ action, item, quantity, max_total, min_total }) => (await me()).api.tradeGameShop(
944
+ action,
945
+ item,
946
+ quantity,
947
+ { maxTotal: max_total, minTotal: min_total }
948
+ ),
869
949
  WRITE,
870
950
  asPlayer
871
951
  );
@@ -911,13 +991,23 @@ function registerGame(server) {
911
991
  tool(
912
992
  server,
913
993
  "arena_battle",
914
- "Read the current battle: both sides' health, moves with uses left, round number and log. Free. Defaults to your live battle.",
915
- { battle_id: z2.string().optional() },
916
- async ({ battle_id }) => {
917
- const { api } = await me();
918
- const id = battle_id ?? (await player2()).battleFleet?.battleId;
919
- if (!id) return { battle: null, note: "No live battle. Use arena_fight to start one." };
920
- return api.readBattle(id);
994
+ "Read a battle: both sides' health, moves with uses left, round number and recent turns. Free. Defaults to your live battle; address reads that player's live battle; battle_id reads one battle directly (finished fights stay readable only briefly).",
995
+ {
996
+ address: z2.string().optional().describe("Whose live battle; omit for yours."),
997
+ battle_id: z2.string().optional().describe("A specific battle, e.g. from game_player arena.liveBattle.")
998
+ },
999
+ async ({ address, battle_id }) => {
1000
+ const { api, address: mine } = await me();
1001
+ const viewer = address ?? mine;
1002
+ if (battle_id) {
1003
+ const b = await findBattle(battle_id);
1004
+ return b ? seenBy(b, viewer) : { battle: null, note: `No battle ${battle_id} on the game or any battle worker.` };
1005
+ }
1006
+ if (!viewer) throw new Error("This server has no wallet; pass address or battle_id.");
1007
+ const p = await player2(viewer);
1008
+ const id = p.battleFleet?.battleId ?? p.activeBattleId;
1009
+ if (!id) return { battle: null, note: address ? `${address} has no live battle.` : "No live battle. Use arena_fight to start one." };
1010
+ return seenBy(p.battle?.id === id ? p.battle : await api.readBattle(id), viewer);
921
1011
  },
922
1012
  READ,
923
1013
  battleView
@@ -955,7 +1045,7 @@ function registerGame(server) {
955
1045
  tool(
956
1046
  server,
957
1047
  "hunt_begin",
958
- "Start a hunt with a companion. Escrows 2 of each berry; Rune and a Scroll are spent only if you try to capture.",
1048
+ "Start a hunt with a companion. Entry fee: 2 of each berry, not refunded. Up to 3 Rune and 1 Scroll go with the run and come back when it ends, unless a capture spends them.",
959
1049
  { monster_id: z2.string().describe("Companion to hunt with (see game_player).") },
960
1050
  async ({ monster_id }) => (await me()).api.beginHunt(monster_id),
961
1051
  WRITE,
@@ -964,14 +1054,14 @@ function registerGame(server) {
964
1054
  tool(
965
1055
  server,
966
1056
  "hunt_state",
967
- "Read your live hunt run: encounter, wild creature, round, status. Free.",
968
- {},
969
- async () => {
1057
+ "Read a live hunt run (yours by default): encounter, wild creature, round, status. Free.",
1058
+ { address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
1059
+ async ({ address }) => {
970
1060
  const { api } = await me();
971
- const route = (await player2()).hunt;
972
- return route ? api.readHunt(route) : { hunt: null, note: "No live hunt. Use hunt_begin." };
1061
+ const route = (await player2(address)).hunt;
1062
+ return route ? api.readHunt(route) : { hunt: null, note: address ? `${address} has no live hunt.` : "No live hunt. Use hunt_begin." };
973
1063
  },
974
- OWN,
1064
+ READ,
975
1065
  huntView
976
1066
  );
977
1067
  tool(
@@ -1006,12 +1096,24 @@ function registerGame(server) {
1006
1096
  WRITE,
1007
1097
  huntView
1008
1098
  );
1099
+ tool(
1100
+ server,
1101
+ "companion_market_browse",
1102
+ "The companion market (priced in in-game Rune): current listings and recent sales. Free, unsigned.",
1103
+ {},
1104
+ async () => {
1105
+ const { api } = await me();
1106
+ const [listings, history] = await Promise.all([api.readMarket(), api.readMarketHistory()]);
1107
+ return { listings, history };
1108
+ },
1109
+ READ
1110
+ );
1009
1111
  tool(
1010
1112
  server,
1011
1113
  "companion_market",
1012
- "The companion market (priced in in-game Rune). browse: listings and recent sales (free). list: sell a companion. buy / cancel: by listing_id.",
1114
+ "Trade companions for in-game Rune (listings: companion_market_browse). list: sell a companion from your collection (never the active one; game_companion store moves it there for 1 Rune). buy / cancel: by listing_id.",
1013
1115
  {
1014
- action: z2.enum(["browse", "list", "buy", "cancel"]),
1116
+ action: z2.enum(["list", "buy", "cancel"]),
1015
1117
  monster_id: z2.string().optional(),
1016
1118
  price: z2.number().int().min(1).max(1e6).optional(),
1017
1119
  listing_id: z2.string().optional()
@@ -1019,8 +1121,6 @@ function registerGame(server) {
1019
1121
  async ({ action, monster_id, price, listing_id }) => {
1020
1122
  const { api } = await me();
1021
1123
  switch (action) {
1022
- case "browse":
1023
- return { listings: await api.readMarket(), history: await api.readMarketHistory() };
1024
1124
  case "list":
1025
1125
  return api.listMonster(need(monster_id, "monster_id"), need(price, "price"));
1026
1126
  case "buy":
@@ -1048,7 +1148,7 @@ function registerGame(server) {
1048
1148
  tool(
1049
1149
  server,
1050
1150
  "element_claim",
1051
- "Claim today's free TEST element tokens. Each wallet gets one element per UTC day, chosen by the contract; this claims it.",
1151
+ "Claim today's free TEST element tokens: 1-10 of one element (chosen by the contract) per wallet per UTC day, until that element's 2,000-token daily cap across all wallets is spent; after that, claims are refused until the next day.",
1052
1152
  {},
1053
1153
  async () => {
1054
1154
  const { api, address } = await me();
@@ -1058,9 +1158,6 @@ function registerGame(server) {
1058
1158
  WRITE
1059
1159
  );
1060
1160
  }
1061
- async function battleView(b) {
1062
- return b?.id ? battle(b, (await me()).address) : b;
1063
- }
1064
1161
  async function attackView(p) {
1065
1162
  if (!p?.address) return p;
1066
1163
  const { address, gold, battlesRemaining, wins, losses, arenaLast } = p;
@@ -1077,16 +1174,18 @@ function need(value, name) {
1077
1174
  if (value === void 0 || value === null || value === "") throw new Error(`${name} is required for this action`);
1078
1175
  return value;
1079
1176
  }
1080
- var FACTIONS, BERRIES, asPlayer, huntView;
1177
+ var FACTIONS, BERRIES, SHOP_ITEMS, seenBy, asPlayer, battleView, huntView;
1081
1178
  var init_game = __esm({
1082
1179
  "src/game.mjs"() {
1083
- "use strict";
1084
1180
  init_runtime();
1085
1181
  init_tool();
1086
1182
  init_views();
1087
1183
  FACTIONS = ["Sky Nomads", "Aqua Guardians", "Inferno Blades", "Stone Titans"];
1088
1184
  BERRIES = ["air_berry", "fire_berry", "rock_berry", "water_berry"];
1185
+ SHOP_ITEMS = [...BERRIES, "scroll", "rune"];
1186
+ seenBy = (b, address) => b && typeof b === "object" ? Object.defineProperty(b, "viewer", { value: address, enumerable: false, configurable: true }) : b;
1089
1187
  asPlayer = (r) => r?.address && r?.inventory ? player(r) : r;
1188
+ battleView = (b) => b?.id ? battle(b, b.viewer) : b;
1090
1189
  huntView = (r) => r?.runId ? hunt(r) : asPlayer(r);
1091
1190
  }
1092
1191
  });
@@ -1111,9 +1210,14 @@ function toWire(t, m, price, quantity) {
1111
1210
  const baseAtoms = parseAmount(quantity, baseDen);
1112
1211
  if (baseAtoms % lot !== 0n) throw new Error(`Quantity moves in steps of ${formatAtoms(lot, baseDen)} ${m.base}.`);
1113
1212
  const perUnit = parseAmount(price, t.den(m.quote));
1114
- const perLot = perUnit * lot / 10n ** BigInt(baseDen);
1115
- if (perLot <= 0n) throw new Error("Price is below one quote atom per lot.");
1116
- return { lots: (baseAtoms / lot).toString(), perLot: perLot.toString() };
1213
+ const unit = 10n ** BigInt(baseDen);
1214
+ const tick = BigInt(Math.max(1, Number(m.tick ?? 1)));
1215
+ const scaled = perUnit * lot;
1216
+ if (scaled % unit !== 0n || scaled / unit % tick !== 0n) {
1217
+ const step = tick * unit % lot === 0n ? formatAtoms(tick * unit / lot, t.den(m.quote)) : null;
1218
+ throw new Error(step ? `Price moves in steps of ${step} ${m.quote} per ${m.base}.` : `Price must be a whole multiple of ${tick} ${m.quote} atoms per lot of ${formatAtoms(lot, baseDen)} ${m.base}.`);
1219
+ }
1220
+ return { lots: (baseAtoms / lot).toString(), perLot: (scaled / unit).toString() };
1117
1221
  }
1118
1222
  function registerExchange(server) {
1119
1223
  tool(
@@ -1138,22 +1242,21 @@ function registerExchange(server) {
1138
1242
  const { api } = await runtime();
1139
1243
  const book = (await api.readVenueBook(t.process))?.[m.id];
1140
1244
  if (!book) return { market: m.id, book: null };
1141
- const unitPrice = (perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
1142
- const units = (lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
1143
- const level = (l) => ({ price: unitPrice(l.price), quantity: units(l.quantity), orders: l.orders });
1245
+ const price = (perLot) => unitPrice(t, m, perLot);
1246
+ const level = (l) => ({ price: price(l.price), quantity: units(t, m, l.quantity), orders: l.orders });
1144
1247
  return {
1145
1248
  market: m.id,
1146
1249
  units: `price in ${m.quote} per 1 ${m.base}; quantity in ${m.base}`,
1147
- bestBid: book.bestBid ? unitPrice(book.bestBid) : null,
1148
- bestAsk: book.bestAsk ? unitPrice(book.bestAsk) : null,
1250
+ bestBid: book.bestBid ? price(book.bestBid) : null,
1251
+ bestAsk: book.bestAsk ? price(book.bestAsk) : null,
1149
1252
  bids: (book.depth?.bids ?? []).slice(0, 10).map(level),
1150
1253
  asks: (book.depth?.asks ?? []).slice(0, 10).map(level),
1151
- band: book.band && { low: unitPrice(book.band.low), high: unitPrice(book.band.high) },
1254
+ band: book.band && { low: price(book.band.low), high: price(book.band.high) },
1152
1255
  today: book.candles?.at(-1) && {
1153
- open: unitPrice(book.candles.at(-1).o),
1154
- high: unitPrice(book.candles.at(-1).h),
1155
- low: unitPrice(book.candles.at(-1).l),
1156
- close: unitPrice(book.candles.at(-1).c),
1256
+ open: price(book.candles.at(-1).o),
1257
+ high: price(book.candles.at(-1).h),
1258
+ low: price(book.candles.at(-1).l),
1259
+ close: price(book.candles.at(-1).c),
1157
1260
  trades: book.candles.at(-1).n
1158
1261
  }
1159
1262
  };
@@ -1163,7 +1266,7 @@ function registerExchange(server) {
1163
1266
  tool(
1164
1267
  server,
1165
1268
  "exchange_position",
1166
- "Balances at a venue (free, in orders) with open orders and recent fills; yours unless address is given. Free.",
1269
+ "Balances at a venue (free, and where each asset sits) with open orders and recent fills, all in human units; yours unless address is given. Free.",
1167
1270
  { venue: VENUE, address: z3.string().optional() },
1168
1271
  async ({ venue, address }) => {
1169
1272
  const t = await terms(venue);
@@ -1171,7 +1274,33 @@ function registerExchange(server) {
1171
1274
  const who = address ?? wallets?.ao.address;
1172
1275
  if (!who) throw new Error("This server has no wallet; pass an address.");
1173
1276
  const p = await api.readVenuePosition(t.process, who);
1174
- return { ...p, freeUnits: human(t, p.free) };
1277
+ const market = (row) => t.markets[row.market] ?? marketFor(t, row.item);
1278
+ const order = ({ lot, ...o }) => {
1279
+ const m = { ...market(o), ...lot ? { lot } : {} };
1280
+ return {
1281
+ ...o,
1282
+ price: unitPrice(t, m, o.price),
1283
+ quantity: units(t, m, o.quantity),
1284
+ ...o.remaining !== void 0 ? { remaining: units(t, m, o.remaining) } : {}
1285
+ };
1286
+ };
1287
+ const fill = (f) => {
1288
+ const m = market(f);
1289
+ return {
1290
+ ...f,
1291
+ price: unitPrice(t, m, f.price),
1292
+ quantity: units(t, m, f.quantity),
1293
+ ...f.fee !== void 0 ? { fee: formatAtoms(BigInt(f.fee), t.den(m.quote)) } : {}
1294
+ };
1295
+ };
1296
+ return {
1297
+ account: p.account ?? who,
1298
+ units: "price in quote per 1 base unit; quantity in base units; fee in quote",
1299
+ free: human(t, p.free),
1300
+ ...p.locations ? { locations: Object.fromEntries(Object.entries(p.locations).map(([asset, at]) => [asset, Object.fromEntries(Object.entries(at).map(([where, atoms]) => [where, formatAtoms(atoms, t.den(asset))]))])) } : {},
1301
+ orders: (p.orders ?? []).map(order),
1302
+ fills: (p.fills ?? []).map(fill)
1303
+ };
1175
1304
  },
1176
1305
  READ
1177
1306
  );
@@ -1269,14 +1398,15 @@ function registerExchange(server) {
1269
1398
  WRITE
1270
1399
  );
1271
1400
  }
1272
- var VENUE, venueProcess, human;
1401
+ var VENUE, venueProcess, unitPrice, units, human;
1273
1402
  var init_exchange = __esm({
1274
1403
  "src/exchange.mjs"() {
1275
- "use strict";
1276
1404
  init_runtime();
1277
1405
  init_tool();
1278
1406
  VENUE = z3.enum(["game", "token"]).describe("game: items vs Gold. token: element tokens vs TEST-RUNE.");
1279
1407
  venueProcess = (venue) => venue === "game" ? graph.processes.internalVenue : graph.processes.externalVenue;
1408
+ unitPrice = (t, m, perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
1409
+ units = (t, m, lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
1280
1410
  human = (t, free) => Object.fromEntries(Object.entries(free ?? {}).map(([asset, atoms]) => [asset, formatAtoms(atoms, t.den(asset))]));
1281
1411
  }
1282
1412
  });
@@ -1708,7 +1838,6 @@ function registerBridge(server) {
1708
1838
  var DAILY_FAUCET, DAY_MS, CHAIN, PUSD, faucetToken, pusdBalance;
1709
1839
  var init_bridge = __esm({
1710
1840
  "src/bridge.mjs"() {
1711
- "use strict";
1712
1841
  init_runtime();
1713
1842
  init_tool();
1714
1843
  DAILY_FAUCET = 100000000n;
@@ -1878,7 +2007,6 @@ function registerDao(server) {
1878
2007
  var RUNE, DEN, tags, runeView;
1879
2008
  var init_dao = __esm({
1880
2009
  "src/dao.mjs"() {
1881
- "use strict";
1882
2010
  init_runtime();
1883
2011
  init_tool();
1884
2012
  RUNE = graph.processes.rune;
@@ -2068,7 +2196,6 @@ function registerVaults(server) {
2068
2196
  var VENUE2, STRATEGY, venueProcess2, DAY_MS2, pct, iso2;
2069
2197
  var init_vaults = __esm({
2070
2198
  "src/vaults.mjs"() {
2071
- "use strict";
2072
2199
  init_runtime();
2073
2200
  init_tool();
2074
2201
  VENUE2 = z6.enum(["game", "token"]).describe("game: items vs Gold. token: element tokens vs TEST-RUNE.");
@@ -2154,7 +2281,6 @@ ${where}` }] };
2154
2281
  var GAME, TRADE, URI, HERE2, link, VIEWS;
2155
2282
  var init_show = __esm({
2156
2283
  "src/show.mjs"() {
2157
- "use strict";
2158
2284
  init_runtime();
2159
2285
  GAME = (process.env.PARALITH_GAME_URL || "https://game.paralithmarkets.com").replace(/\/$/, "");
2160
2286
  TRADE = (process.env.PARALITH_TRADE_URL || "https://trade.paralithmarkets.com").replace(/\/$/, "");
@@ -2272,7 +2398,6 @@ function registerProgress(server) {
2272
2398
  var VENUES, count;
2273
2399
  var init_progress = __esm({
2274
2400
  "src/progress.mjs"() {
2275
- "use strict";
2276
2401
  init_runtime();
2277
2402
  init_tool();
2278
2403
  init_agents();
@@ -2294,9 +2419,7 @@ import { z as z9 } from "zod";
2294
2419
  function buildServer() {
2295
2420
  const server = new McpServer(
2296
2421
  { name: "paralith", version: VERSION },
2297
- { instructions: READ_ONLY ? `${READ_ONLY_NOTE}
2298
-
2299
- ${INSTRUCTIONS}` : INSTRUCTIONS }
2422
+ { instructions: READ_ONLY ? READ_ONLY_INSTRUCTIONS : INSTRUCTIONS }
2300
2423
  );
2301
2424
  tool(
2302
2425
  server,
@@ -2323,7 +2446,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2323
2446
  tool(
2324
2447
  server,
2325
2448
  "read_state",
2326
- `Read any published key of any process, free: /<process>~process@1.0/now/<key>. process is an id or an alias (${Object.keys(PROCESS_ALIASES).join(", ")}). Returns null when absent.`,
2449
+ `Read any published key of any process, free: /<process>~process@1.0/now/<key>. process is an id or an alias (${Object.keys(PROCESS_ALIASES).join(", ")}). Returns null when absent. Common keys: game: player-<address>, catalogref (the catalog's id, not catalog), leaderboard, factions, arenatiers, challenges, economy, economybook, market, markethistory, battlefleet, huntconfig, access. rune and *-token: balance-<address>, tokeninfo (not info), totalsupply. pusd: balance-<address>, bridgeinfo, withdrawals. dao: daoinfo, proposals. game-venue and token-venue: balance-<address>, vaultinfo, vaultpairs.`,
2327
2450
  { process: z9.string(), key: z9.string() },
2328
2451
  async ({ process: process2, key }) => readKey(process2, key),
2329
2452
  READ
@@ -2331,7 +2454,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2331
2454
  tool(
2332
2455
  server,
2333
2456
  "send_message",
2334
- "Escape hatch: sign and send a raw message to any process and return its reply. Prefer the specific tools; use this only for an action they do not cover. Tag values must be strings; never use a tag named Target.",
2457
+ "Escape hatch: sign and send a raw message to any process and return its reply. Prefer the specific tools; use this only for an action they do not cover. Tag values must be strings; never use a tag named Target, Signature or Epoch.",
2335
2458
  {
2336
2459
  process: z9.string(),
2337
2460
  tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
@@ -2339,7 +2462,8 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2339
2462
  push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
2340
2463
  },
2341
2464
  async ({ process: process2, tags: tags2, data, push_outbox }) => {
2342
- if (Object.keys(tags2).some((k) => k.toLowerCase() === "target")) throw new Error("Target is not a usable tag name.");
2465
+ const reserved = Object.keys(tags2).find((k) => ["target", "signature", "epoch"].includes(k.toLowerCase().replace(/[-_]/g, "")));
2466
+ if (reserved) throw new Error(`${reserved} is not a usable tag name (Target, Signature and Epoch are reserved).`);
2343
2467
  const { api } = await runtime();
2344
2468
  return api.rawSend(
2345
2469
  Object.entries(tags2).map(([name, value]) => ({ name, value })),
@@ -2369,10 +2493,9 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2369
2493
  registerProgress(server);
2370
2494
  return server;
2371
2495
  }
2372
- var DOCS, INSTRUCTIONS, READ_ONLY_NOTE;
2496
+ var DOCS, INSTRUCTIONS, READ_ONLY_INSTRUCTIONS;
2373
2497
  var init_build = __esm({
2374
2498
  "src/build.mjs"() {
2375
- "use strict";
2376
2499
  init_runtime();
2377
2500
  init_keys();
2378
2501
  init_tool();
@@ -2389,29 +2512,33 @@ var init_build = __esm({
2389
2512
  Everything is TEST-prefixed while unreleased. Reads are free; every write is a signed message (a few seconds round trip, longer under load).
2390
2513
 
2391
2514
  Start: wallet -> game_player. No account? game_join (pick a faction).
2392
- Core loop: game_daily once a day; game_companion feed/play/quest/claim to keep energy and happiness up and earn exp and Gold;
2393
- game_level_up when exp allows; game_learn_move when an offer is pending.
2515
+ Core loop: game_daily once a day; game_companion feed/play keep energy and happiness up, quest then claim earns exp and Gold;
2516
+ game_level_up when exp allows; game_learn_move when an offer is pending (it expires at the next level-up).
2394
2517
  Arena: arena_enter (25 energy + 25 happiness, 4 fights), arena_fight mode=bot, then arena_attack each round
2395
2518
  (read arena_battle for moves and uses left; Rally and Mend are free once each), arena_end leave when done.
2396
- Hunt: hunt_begin -> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
2519
+ Hunt: hunt_begin (entry fee 2 of each berry, not refunded; up to 3 Rune + 1 Scroll set aside and returned unless a capture spends them)
2520
+ -> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
2397
2521
  Markets: exchange_markets / exchange_book / exchange_order on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE).
2398
2522
  Vaults (pooled liquidity, one amm/active/grid per market): vault_list, vault_position, vault_deposit, vault_shares (withdraw/lock/burn), vault_fund, vault_crank.
2399
2523
  Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
2400
2524
  DAO (stake TEST-RUNE; a passed proposal may send one attached message): dao_info, dao_proposals, dao_stake stake/unstake/withdraw, dao_propose, dao_vote.
2401
2525
 
2402
2526
  HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
2403
- - Gold (in-game only, never leaves the game). Earn: companion play/quest/claim (capped at 60 Gold per 20 h), arena bot wins,
2404
- selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue. Spend: arena stakes, shop buys, game-venue buys.
2527
+ - Gold (in-game only, never leaves the game). Earn: quest claims and arena bot wins, which share one 60 Gold allowance per 20 h
2528
+ (play pays happiness only); selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
2529
+ Spend: arena stakes, shop buys, game-venue buys.
2405
2530
  - Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
2406
- Use: feed the companion, hunt escrow (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
2531
+ Use: feed the companion, the hunt entry fee (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
2407
2532
  - Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
2408
- Get Rune: (1) the shop, only if game_shop quote shows an ask (it often only bids); (2) sell a companion on companion_market;
2409
- (3) the token route, which always works: element_claim (free daily, 1-10 of one element) -> exchange_order venue "token"
2410
- sell that element for TEST-RUNE -> rune_move deposit. Public TEST accounts accrue no Rune from game_daily (pass holders only).
2411
- Spend in-game Rune: game_level_up, hunt capture, companion_market buys. TEST-RUNE: token-venue buys, vaults, dao_stake.
2533
+ Get Rune: (1) the shop, only if game_shop_quote shows an ask (it often only bids); (2) sell a companion on companion_market;
2534
+ (3) the token route: element_claim (free, 1-10 of one element per wallet per UTC day until that element's 2,000/day
2535
+ global cap is spent) -> exchange_order venue "token" sell that element for TEST-RUNE -> rune_move deposit.
2536
+ Public TEST accounts accrue no Rune from game_daily (pass holders only).
2537
+ Spend in-game Rune: game_level_up (ceil(L^2/32) for level L), hunt capture, companion_market buys, game_companion store (1 Rune).
2538
+ TEST-RUNE: token-venue buys, vaults, dao_stake.
2412
2539
  - Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
2413
2540
  - PUSD: bridge in TEST-USDC from Base, Solana or Sui (needs that chain's gas, which the user funds; bridge_faucet gives TEST-USDC).
2414
- No PUSD market is listed yet. Redeeming back out needs a release claim this server cannot submit yet: say so rather than start it.
2541
+ No PUSD market is listed yet. bridge_redeem waits for k attestor signatures and submits the release (needs destination-chain gas).
2415
2542
  - Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
2416
2543
 
2417
2544
  To report how you (or every agent set up on this machine) are doing, call progress (who: "all" for every agent); it also lists what needs doing next.
@@ -2422,8 +2549,24 @@ Show once per thing, not once per round.
2422
2549
 
2423
2550
  Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
2424
2551
  A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
2425
- READ_ONLY_NOTE = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address.
2426
- To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp`;
2552
+ READ_ONLY_INSTRUCTIONS = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address, free.
2553
+ To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
2554
+
2555
+ Paralith on AO (HyperBEAM): Rune Realm (a companion game), two order books, and the PUSD bridge.
2556
+ Everything is TEST-prefixed while unreleased. Pass an address wherever a tool takes one.
2557
+
2558
+ - Game: game_player (account, companions, live battle or hunt), game_catalog (rules, costs, move stats),
2559
+ game_world (factions, arena tiers, open challenges, leaderboard), arena_battle (address or battle_id), hunt_state (address),
2560
+ game_shop_quote (NPC shop prices and stock), companion_market_browse (listings and recent sales).
2561
+ - Markets: exchange_markets / exchange_book / exchange_position on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE);
2562
+ vault_list, vault_position.
2563
+ - Bridge and DAO: bridge_info, bridge_status, dao_info, dao_proposals, dao_stake_info.
2564
+ - Anything else: progress (one address across everything), read_state (any published key), protocol_docs.
2565
+
2566
+ SHOW IT. Watching beats reading: call show for anything worth seeing (a fight as it happens, a companion, a book, a vault,
2567
+ a bridge transfer). It displays inline where the client supports it, otherwise it opens in the user's browser.
2568
+
2569
+ Docs: ${DOCS}/llms-full.txt.`;
2427
2570
  }
2428
2571
  });
2429
2572