paralith-mcp 0.4.5 → 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.
Files changed (3) hide show
  1. package/dist/http.mjs +239 -86
  2. package/dist/server.mjs +248 -89
  3. package/package.json +1 -1
package/dist/http.mjs CHANGED
@@ -305,8 +305,62 @@ import { z as z9 } from "zod";
305
305
  // src/tool.mjs
306
306
  import { z } from "zod";
307
307
 
308
+ // package.json
309
+ var package_default = {
310
+ name: "paralith-mcp",
311
+ version: "0.4.6",
312
+ 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).",
313
+ license: "UNLICENSED",
314
+ homepage: "https://docs.paralithmarkets.com/ai/mcp",
315
+ keywords: [
316
+ "mcp",
317
+ "model-context-protocol",
318
+ "ao",
319
+ "hyperbeam",
320
+ "arweave",
321
+ "game",
322
+ "agent"
323
+ ],
324
+ type: "module",
325
+ bin: {
326
+ "paralith-mcp": "dist/server.mjs",
327
+ "paralith-bridge": "dist/bridge.mjs"
328
+ },
329
+ files: [
330
+ "dist",
331
+ "README.md"
332
+ ],
333
+ publishConfig: {
334
+ access: "public"
335
+ },
336
+ scripts: {
337
+ build: "node scripts/build-client.mjs && node scripts/tools-doc.mjs",
338
+ "tools-doc": "node scripts/tools-doc.mjs",
339
+ start: "node src/server.mjs",
340
+ "start:http": "node src/http.mjs",
341
+ test: "node --test test/*.test.mjs"
342
+ },
343
+ engines: {
344
+ node: ">=20"
345
+ },
346
+ dependencies: {
347
+ "@modelcontextprotocol/ext-apps": "^2.0.3",
348
+ "@modelcontextprotocol/sdk": "^1.32.1",
349
+ "@mysten/sui": "^2.34.0",
350
+ "@scure/bip39": "^2.4.0",
351
+ "@solana/web3.js": "^1.99.0",
352
+ "arweave-mnemonic-keys": "^0.0.9",
353
+ lodash: "^4.18.1",
354
+ viem: "^2.57.3",
355
+ zod: "^4.6.5"
356
+ },
357
+ devDependencies: {
358
+ esbuild: "^0.28.2"
359
+ }
360
+ };
361
+
308
362
  // src/update.mjs
309
- var VERSION = "0.4.2";
363
+ var VERSION = package_default.version;
310
364
  var CHECK_MS = 6 * 36e5;
311
365
  var NOTE_MS = 36e5;
312
366
  var latest = null;
@@ -443,13 +497,20 @@ function side(s) {
443
497
  freeMovesUsed: Object.keys(s.freeUsed ?? {})
444
498
  };
445
499
  }
500
+ function sideOf(b, address) {
501
+ if (!address) return null;
502
+ if ((b.accepterAddress ?? b.accepter?.address) === address) return "accepter";
503
+ if ((b.challengerAddress ?? b.challenger?.address) === address) return "challenger";
504
+ return null;
505
+ }
446
506
  function battle(b, address) {
447
507
  if (!b || typeof b !== "object") return b;
448
- const mine = b.accepterAddress === address ? "accepter" : "challenger";
508
+ const mine = sideOf(b, address);
449
509
  const theirs = mine === "accepter" ? "challenger" : "accepter";
510
+ const label = (s) => !mine ? s : s === mine ? "you" : "opponent";
450
511
  const turns = (b.turns ?? []).filter((t) => t.round === b.round - 1 || t.round === b.round).map((t) => ({
451
512
  round: t.round,
452
- by: t.attacker === mine ? "you" : "opponent",
513
+ by: label(t.attacker),
453
514
  move: t.move,
454
515
  damage: t.healthDamage,
455
516
  shieldDamage: t.shieldDamage,
@@ -458,6 +519,7 @@ function battle(b, address) {
458
519
  superEffective: t.superEffective,
459
520
  notEffective: t.notEffective
460
521
  }));
522
+ const sides = mine ? { you: side(b[mine]), opponent: side(b[theirs]) } : { challenger: side(b.challenger), accepter: side(b.accepter) };
461
523
  return {
462
524
  id: b.id,
463
525
  kind: b.kind,
@@ -465,8 +527,7 @@ function battle(b, address) {
465
527
  round: b.round,
466
528
  ...b.winner ? { winner: b.winner } : {},
467
529
  ...b.waitingOn?.length ? { waitingOn: b.waitingOn } : {},
468
- you: side(b[mine]),
469
- opponent: side(b[theirs]),
530
+ ...sides,
470
531
  recentTurns: turns
471
532
  };
472
533
  }
@@ -483,22 +544,31 @@ function hunt(run) {
483
544
  // src/game.mjs
484
545
  var FACTIONS = ["Sky Nomads", "Aqua Guardians", "Inferno Blades", "Stone Titans"];
485
546
  var BERRIES = ["air_berry", "fire_berry", "rock_berry", "water_berry"];
547
+ var SHOP_ITEMS = [...BERRIES, "scroll", "rune"];
486
548
  async function me() {
487
549
  const { api, wallets } = await runtime();
488
550
  return { api, address: wallets?.ao.address };
489
551
  }
490
- async function player2() {
491
- const { api, address } = await me();
492
- if (!address) throw new Error("This server has no wallet; pass an address or battle_id.");
493
- const p = await api.readPlayer(address);
494
- if (!p) throw new Error("This wallet has no Rune Realm account yet. Call game_join first.");
552
+ async function player2(address) {
553
+ const { api, address: mine } = await me();
554
+ const who = address ?? mine;
555
+ if (!who) throw new Error("This server has no wallet; pass an address.");
556
+ const p = await api.readPlayer(who);
557
+ if (!p) throw new Error(address ? `${address} has no Rune Realm account.` : "This wallet has no Rune Realm account yet. Call game_join first.");
495
558
  return p;
496
559
  }
560
+ async function findBattle(id) {
561
+ const fleet = await readKey("game", "battlefleet").catch(() => null);
562
+ const workers = (fleet?.workers ?? []).map((w) => w.workerProcessId).filter(Boolean);
563
+ const found = await Promise.all([readKey("game", `battle-op-${id}`), ...workers.map((w) => readKey(w, `battle-${id}`))].map((read) => read.catch(() => null)));
564
+ return found.find((b) => b && typeof b === "object") ?? null;
565
+ }
566
+ var seenBy = (b, address) => b && typeof b === "object" ? Object.defineProperty(b, "viewer", { value: address, enumerable: false, configurable: true }) : b;
497
567
  function registerGame(server) {
498
568
  tool(
499
569
  server,
500
570
  "game_player",
501
- "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.",
571
+ "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.",
502
572
  { address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
503
573
  async ({ address }) => {
504
574
  const { api, address: mine } = await me();
@@ -514,7 +584,13 @@ function registerGame(server) {
514
584
  "game_catalog",
515
585
  "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.",
516
586
  {},
517
- async () => (await me()).api.readCatalog(),
587
+ async () => {
588
+ const catalog = await (await me()).api.readCatalog();
589
+ if (catalog?.levelUp) {
590
+ catalog.levelUp = { ...catalog.levelUp, costDivisor: 32, cost: "ceil(targetLevel^2 / 32) Rune" };
591
+ }
592
+ return catalog;
593
+ },
518
594
  READ
519
595
  );
520
596
  tool(
@@ -536,7 +612,11 @@ function registerGame(server) {
536
612
  factions: factions?.map(({ members, ...f }) => f),
537
613
  arenaTiers: tiers,
538
614
  openChallenges: challenges,
539
- leaderboard: leaderboard?.slice(0, 10)
615
+ leaderboard: leaderboard?.slice(0, 10).map(({ monster, ...row }) => {
616
+ if (!monster) return row;
617
+ const { exp, energy, happiness, ...companion2 } = companion(monster);
618
+ return { ...row, companion: companion2 };
619
+ })
540
620
  };
541
621
  },
542
622
  READ
@@ -553,7 +633,7 @@ function registerGame(server) {
553
633
  tool(
554
634
  server,
555
635
  "game_companion",
556
- "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.",
636
+ "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.",
557
637
  {
558
638
  action: z2.enum(["feed", "play", "quest", "claim", "adopt", "store", "retrieve", "set_active", "transfer"]),
559
639
  monster_id: z2.string().optional().describe("Companion id; omit for the active one."),
@@ -589,7 +669,7 @@ function registerGame(server) {
589
669
  tool(
590
670
  server,
591
671
  "game_level_up",
592
- "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.",
672
+ "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.",
593
673
  { attack: z2.number().int().min(0), defense: z2.number().int().min(0), speed: z2.number().int().min(0), health: z2.number().int().min(0) },
594
674
  async (points) => (await me()).api.levelUp(points),
595
675
  WRITE,
@@ -598,7 +678,7 @@ function registerGame(server) {
598
678
  tool(
599
679
  server,
600
680
  "game_learn_move",
601
- "Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way.",
681
+ "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.",
602
682
  { move: z2.string().describe("The offered move, exactly as in the player record."), replace: z2.string().optional() },
603
683
  async ({ move, replace }) => (await me()).api.learnMove(move, replace),
604
684
  WRITE,
@@ -622,27 +702,31 @@ function registerGame(server) {
622
702
  WRITE,
623
703
  asPlayer
624
704
  );
705
+ tool(
706
+ server,
707
+ "game_shop_quote",
708
+ "The NPC realm shop's current Gold prices (bid/ask) and stock for berries, Scrolls and Rune. Free, unsigned.",
709
+ {},
710
+ async () => (await me()).api.readEconomy(),
711
+ READ
712
+ );
625
713
  tool(
626
714
  server,
627
715
  "game_shop",
628
- "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.",
716
+ "Buy from or sell to the NPC realm shop for Gold (prices: game_shop_quote). Set max_total/min_total to bound slippage.",
629
717
  {
630
- action: z2.enum(["quote", "buy", "sell"]),
631
- item: z2.string().optional().describe("Item id, e.g. fire_berry or scroll."),
632
- quantity: z2.number().int().positive().optional(),
718
+ action: z2.enum(["buy", "sell"]),
719
+ item: z2.enum(SHOP_ITEMS),
720
+ quantity: z2.number().int().positive(),
633
721
  max_total: z2.number().int().optional(),
634
722
  min_total: z2.number().int().optional()
635
723
  },
636
- async ({ action, item, quantity, max_total, min_total }) => {
637
- const { api } = await me();
638
- if (action === "quote") return api.readEconomy();
639
- return api.tradeGameShop(
640
- action,
641
- need(item, "item"),
642
- need(quantity, "quantity"),
643
- { maxTotal: max_total, minTotal: min_total }
644
- );
645
- },
724
+ async ({ action, item, quantity, max_total, min_total }) => (await me()).api.tradeGameShop(
725
+ action,
726
+ item,
727
+ quantity,
728
+ { maxTotal: max_total, minTotal: min_total }
729
+ ),
646
730
  WRITE,
647
731
  asPlayer
648
732
  );
@@ -688,13 +772,23 @@ function registerGame(server) {
688
772
  tool(
689
773
  server,
690
774
  "arena_battle",
691
- "Read the current battle: both sides' health, moves with uses left, round number and log. Free. Defaults to your live battle.",
692
- { battle_id: z2.string().optional() },
693
- async ({ battle_id }) => {
694
- const { api } = await me();
695
- const id = battle_id ?? (await player2()).battleFleet?.battleId;
696
- if (!id) return { battle: null, note: "No live battle. Use arena_fight to start one." };
697
- return api.readBattle(id);
775
+ "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).",
776
+ {
777
+ address: z2.string().optional().describe("Whose live battle; omit for yours."),
778
+ battle_id: z2.string().optional().describe("A specific battle, e.g. from game_player arena.liveBattle.")
779
+ },
780
+ async ({ address, battle_id }) => {
781
+ const { api, address: mine } = await me();
782
+ const viewer = address ?? mine;
783
+ if (battle_id) {
784
+ const b = await findBattle(battle_id);
785
+ return b ? seenBy(b, viewer) : { battle: null, note: `No battle ${battle_id} on the game or any battle worker.` };
786
+ }
787
+ if (!viewer) throw new Error("This server has no wallet; pass address or battle_id.");
788
+ const p = await player2(viewer);
789
+ const id = p.battleFleet?.battleId ?? p.activeBattleId;
790
+ if (!id) return { battle: null, note: address ? `${address} has no live battle.` : "No live battle. Use arena_fight to start one." };
791
+ return seenBy(p.battle?.id === id ? p.battle : await api.readBattle(id), viewer);
698
792
  },
699
793
  READ,
700
794
  battleView
@@ -732,7 +826,7 @@ function registerGame(server) {
732
826
  tool(
733
827
  server,
734
828
  "hunt_begin",
735
- "Start a hunt with a companion. Escrows 2 of each berry; Rune and a Scroll are spent only if you try to capture.",
829
+ "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.",
736
830
  { monster_id: z2.string().describe("Companion to hunt with (see game_player).") },
737
831
  async ({ monster_id }) => (await me()).api.beginHunt(monster_id),
738
832
  WRITE,
@@ -741,14 +835,14 @@ function registerGame(server) {
741
835
  tool(
742
836
  server,
743
837
  "hunt_state",
744
- "Read your live hunt run: encounter, wild creature, round, status. Free.",
745
- {},
746
- async () => {
838
+ "Read a live hunt run (yours by default): encounter, wild creature, round, status. Free.",
839
+ { address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
840
+ async ({ address }) => {
747
841
  const { api } = await me();
748
- const route = (await player2()).hunt;
749
- return route ? api.readHunt(route) : { hunt: null, note: "No live hunt. Use hunt_begin." };
842
+ const route = (await player2(address)).hunt;
843
+ return route ? api.readHunt(route) : { hunt: null, note: address ? `${address} has no live hunt.` : "No live hunt. Use hunt_begin." };
750
844
  },
751
- OWN,
845
+ READ,
752
846
  huntView
753
847
  );
754
848
  tool(
@@ -783,12 +877,24 @@ function registerGame(server) {
783
877
  WRITE,
784
878
  huntView
785
879
  );
880
+ tool(
881
+ server,
882
+ "companion_market_browse",
883
+ "The companion market (priced in in-game Rune): current listings and recent sales. Free, unsigned.",
884
+ {},
885
+ async () => {
886
+ const { api } = await me();
887
+ const [listings, history] = await Promise.all([api.readMarket(), api.readMarketHistory()]);
888
+ return { listings, history };
889
+ },
890
+ READ
891
+ );
786
892
  tool(
787
893
  server,
788
894
  "companion_market",
789
- "The companion market (priced in in-game Rune). browse: listings and recent sales (free). list: sell a companion. buy / cancel: by listing_id.",
895
+ "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.",
790
896
  {
791
- action: z2.enum(["browse", "list", "buy", "cancel"]),
897
+ action: z2.enum(["list", "buy", "cancel"]),
792
898
  monster_id: z2.string().optional(),
793
899
  price: z2.number().int().min(1).max(1e6).optional(),
794
900
  listing_id: z2.string().optional()
@@ -796,8 +902,6 @@ function registerGame(server) {
796
902
  async ({ action, monster_id, price, listing_id }) => {
797
903
  const { api } = await me();
798
904
  switch (action) {
799
- case "browse":
800
- return { listings: await api.readMarket(), history: await api.readMarketHistory() };
801
905
  case "list":
802
906
  return api.listMonster(need(monster_id, "monster_id"), need(price, "price"));
803
907
  case "buy":
@@ -825,7 +929,7 @@ function registerGame(server) {
825
929
  tool(
826
930
  server,
827
931
  "element_claim",
828
- "Claim today's free TEST element tokens. Each wallet gets one element per UTC day, chosen by the contract; this claims it.",
932
+ "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.",
829
933
  {},
830
934
  async () => {
831
935
  const { api, address } = await me();
@@ -836,9 +940,7 @@ function registerGame(server) {
836
940
  );
837
941
  }
838
942
  var asPlayer = (r) => r?.address && r?.inventory ? player(r) : r;
839
- async function battleView(b) {
840
- return b?.id ? battle(b, (await me()).address) : b;
841
- }
943
+ var battleView = (b) => b?.id ? battle(b, b.viewer) : b;
842
944
  async function attackView(p) {
843
945
  if (!p?.address) return p;
844
946
  const { address, gold, battlesRemaining, wins, losses, arenaLast } = p;
@@ -879,10 +981,17 @@ function toWire(t, m, price, quantity) {
879
981
  const baseAtoms = parseAmount(quantity, baseDen);
880
982
  if (baseAtoms % lot !== 0n) throw new Error(`Quantity moves in steps of ${formatAtoms(lot, baseDen)} ${m.base}.`);
881
983
  const perUnit = parseAmount(price, t.den(m.quote));
882
- const perLot = perUnit * lot / 10n ** BigInt(baseDen);
883
- if (perLot <= 0n) throw new Error("Price is below one quote atom per lot.");
884
- return { lots: (baseAtoms / lot).toString(), perLot: perLot.toString() };
984
+ const unit = 10n ** BigInt(baseDen);
985
+ const tick = BigInt(Math.max(1, Number(m.tick ?? 1)));
986
+ const scaled = perUnit * lot;
987
+ if (scaled % unit !== 0n || scaled / unit % tick !== 0n) {
988
+ const step = tick * unit % lot === 0n ? formatAtoms(tick * unit / lot, t.den(m.quote)) : null;
989
+ 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}.`);
990
+ }
991
+ return { lots: (baseAtoms / lot).toString(), perLot: (scaled / unit).toString() };
885
992
  }
993
+ var unitPrice = (t, m, perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
994
+ var units = (t, m, lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
886
995
  var human = (t, free) => Object.fromEntries(Object.entries(free ?? {}).map(([asset, atoms]) => [asset, formatAtoms(atoms, t.den(asset))]));
887
996
  function registerExchange(server) {
888
997
  tool(
@@ -907,22 +1016,21 @@ function registerExchange(server) {
907
1016
  const { api } = await runtime();
908
1017
  const book = (await api.readVenueBook(t.process))?.[m.id];
909
1018
  if (!book) return { market: m.id, book: null };
910
- const unitPrice = (perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
911
- const units = (lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
912
- const level = (l) => ({ price: unitPrice(l.price), quantity: units(l.quantity), orders: l.orders });
1019
+ const price = (perLot) => unitPrice(t, m, perLot);
1020
+ const level = (l) => ({ price: price(l.price), quantity: units(t, m, l.quantity), orders: l.orders });
913
1021
  return {
914
1022
  market: m.id,
915
1023
  units: `price in ${m.quote} per 1 ${m.base}; quantity in ${m.base}`,
916
- bestBid: book.bestBid ? unitPrice(book.bestBid) : null,
917
- bestAsk: book.bestAsk ? unitPrice(book.bestAsk) : null,
1024
+ bestBid: book.bestBid ? price(book.bestBid) : null,
1025
+ bestAsk: book.bestAsk ? price(book.bestAsk) : null,
918
1026
  bids: (book.depth?.bids ?? []).slice(0, 10).map(level),
919
1027
  asks: (book.depth?.asks ?? []).slice(0, 10).map(level),
920
- band: book.band && { low: unitPrice(book.band.low), high: unitPrice(book.band.high) },
1028
+ band: book.band && { low: price(book.band.low), high: price(book.band.high) },
921
1029
  today: book.candles?.at(-1) && {
922
- open: unitPrice(book.candles.at(-1).o),
923
- high: unitPrice(book.candles.at(-1).h),
924
- low: unitPrice(book.candles.at(-1).l),
925
- close: unitPrice(book.candles.at(-1).c),
1030
+ open: price(book.candles.at(-1).o),
1031
+ high: price(book.candles.at(-1).h),
1032
+ low: price(book.candles.at(-1).l),
1033
+ close: price(book.candles.at(-1).c),
926
1034
  trades: book.candles.at(-1).n
927
1035
  }
928
1036
  };
@@ -932,7 +1040,7 @@ function registerExchange(server) {
932
1040
  tool(
933
1041
  server,
934
1042
  "exchange_position",
935
- "Balances at a venue (free, in orders) with open orders and recent fills; yours unless address is given. Free.",
1043
+ "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.",
936
1044
  { venue: VENUE, address: z3.string().optional() },
937
1045
  async ({ venue, address }) => {
938
1046
  const t = await terms(venue);
@@ -940,7 +1048,33 @@ function registerExchange(server) {
940
1048
  const who = address ?? wallets?.ao.address;
941
1049
  if (!who) throw new Error("This server has no wallet; pass an address.");
942
1050
  const p = await api.readVenuePosition(t.process, who);
943
- return { ...p, freeUnits: human(t, p.free) };
1051
+ const market = (row) => t.markets[row.market] ?? marketFor(t, row.item);
1052
+ const order = ({ lot, ...o }) => {
1053
+ const m = { ...market(o), ...lot ? { lot } : {} };
1054
+ return {
1055
+ ...o,
1056
+ price: unitPrice(t, m, o.price),
1057
+ quantity: units(t, m, o.quantity),
1058
+ ...o.remaining !== void 0 ? { remaining: units(t, m, o.remaining) } : {}
1059
+ };
1060
+ };
1061
+ const fill = (f) => {
1062
+ const m = market(f);
1063
+ return {
1064
+ ...f,
1065
+ price: unitPrice(t, m, f.price),
1066
+ quantity: units(t, m, f.quantity),
1067
+ ...f.fee !== void 0 ? { fee: formatAtoms(BigInt(f.fee), t.den(m.quote)) } : {}
1068
+ };
1069
+ };
1070
+ return {
1071
+ account: p.account ?? who,
1072
+ units: "price in quote per 1 base unit; quantity in base units; fee in quote",
1073
+ free: human(t, p.free),
1074
+ ...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))]))])) } : {},
1075
+ orders: (p.orders ?? []).map(order),
1076
+ fills: (p.fills ?? []).map(fill)
1077
+ };
944
1078
  },
945
1079
  READ
946
1080
  );
@@ -2005,29 +2139,33 @@ var INSTRUCTIONS = `Paralith on AO (HyperBEAM): Rune Realm (a companion game), t
2005
2139
  Everything is TEST-prefixed while unreleased. Reads are free; every write is a signed message (a few seconds round trip, longer under load).
2006
2140
 
2007
2141
  Start: wallet -> game_player. No account? game_join (pick a faction).
2008
- Core loop: game_daily once a day; game_companion feed/play/quest/claim to keep energy and happiness up and earn exp and Gold;
2009
- game_level_up when exp allows; game_learn_move when an offer is pending.
2142
+ Core loop: game_daily once a day; game_companion feed/play keep energy and happiness up, quest then claim earns exp and Gold;
2143
+ game_level_up when exp allows; game_learn_move when an offer is pending (it expires at the next level-up).
2010
2144
  Arena: arena_enter (25 energy + 25 happiness, 4 fights), arena_fight mode=bot, then arena_attack each round
2011
2145
  (read arena_battle for moves and uses left; Rally and Mend are free once each), arena_end leave when done.
2012
- Hunt: hunt_begin -> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
2146
+ 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)
2147
+ -> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
2013
2148
  Markets: exchange_markets / exchange_book / exchange_order on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE).
2014
2149
  Vaults (pooled liquidity, one amm/active/grid per market): vault_list, vault_position, vault_deposit, vault_shares (withdraw/lock/burn), vault_fund, vault_crank.
2015
2150
  Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
2016
2151
  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.
2017
2152
 
2018
2153
  HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
2019
- - Gold (in-game only, never leaves the game). Earn: companion play/quest/claim (capped at 60 Gold per 20 h), arena bot wins,
2020
- selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue. Spend: arena stakes, shop buys, game-venue buys.
2154
+ - Gold (in-game only, never leaves the game). Earn: quest claims and arena bot wins, which share one 60 Gold allowance per 20 h
2155
+ (play pays happiness only); selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
2156
+ Spend: arena stakes, shop buys, game-venue buys.
2021
2157
  - Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
2022
- Use: feed the companion, hunt escrow (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
2158
+ Use: feed the companion, the hunt entry fee (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
2023
2159
  - Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
2024
- Get Rune: (1) the shop, only if game_shop quote shows an ask (it often only bids); (2) sell a companion on companion_market;
2025
- (3) the token route, which always works: element_claim (free daily, 1-10 of one element) -> exchange_order venue "token"
2026
- sell that element for TEST-RUNE -> rune_move deposit. Public TEST accounts accrue no Rune from game_daily (pass holders only).
2027
- Spend in-game Rune: game_level_up, hunt capture, companion_market buys. TEST-RUNE: token-venue buys, vaults, dao_stake.
2160
+ Get Rune: (1) the shop, only if game_shop_quote shows an ask (it often only bids); (2) sell a companion on companion_market;
2161
+ (3) the token route: element_claim (free, 1-10 of one element per wallet per UTC day until that element's 2,000/day
2162
+ global cap is spent) -> exchange_order venue "token" sell that element for TEST-RUNE -> rune_move deposit.
2163
+ Public TEST accounts accrue no Rune from game_daily (pass holders only).
2164
+ Spend in-game Rune: game_level_up (ceil(L^2/32) for level L), hunt capture, companion_market buys, game_companion store (1 Rune).
2165
+ TEST-RUNE: token-venue buys, vaults, dao_stake.
2028
2166
  - Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
2029
2167
  - PUSD: bridge in TEST-USDC from Base, Solana or Sui (needs that chain's gas, which the user funds; bridge_faucet gives TEST-USDC).
2030
- No PUSD market is listed yet. Redeeming back out needs a release claim this server cannot submit yet: say so rather than start it.
2168
+ No PUSD market is listed yet. bridge_redeem waits for k attestor signatures and submits the release (needs destination-chain gas).
2031
2169
  - Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
2032
2170
 
2033
2171
  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.
@@ -2038,14 +2176,28 @@ Show once per thing, not once per round.
2038
2176
 
2039
2177
  Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
2040
2178
  A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
2041
- var READ_ONLY_NOTE = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address.
2042
- To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp`;
2179
+ var READ_ONLY_INSTRUCTIONS = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address, free.
2180
+ To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
2181
+
2182
+ Paralith on AO (HyperBEAM): Rune Realm (a companion game), two order books, and the PUSD bridge.
2183
+ Everything is TEST-prefixed while unreleased. Pass an address wherever a tool takes one.
2184
+
2185
+ - Game: game_player (account, companions, live battle or hunt), game_catalog (rules, costs, move stats),
2186
+ game_world (factions, arena tiers, open challenges, leaderboard), arena_battle (address or battle_id), hunt_state (address),
2187
+ game_shop_quote (NPC shop prices and stock), companion_market_browse (listings and recent sales).
2188
+ - Markets: exchange_markets / exchange_book / exchange_position on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE);
2189
+ vault_list, vault_position.
2190
+ - Bridge and DAO: bridge_info, bridge_status, dao_info, dao_proposals, dao_stake_info.
2191
+ - Anything else: progress (one address across everything), read_state (any published key), protocol_docs.
2192
+
2193
+ SHOW IT. Watching beats reading: call show for anything worth seeing (a fight as it happens, a companion, a book, a vault,
2194
+ a bridge transfer). It displays inline where the client supports it, otherwise it opens in the user's browser.
2195
+
2196
+ Docs: ${DOCS}/llms-full.txt.`;
2043
2197
  function buildServer() {
2044
2198
  const server = new McpServer(
2045
2199
  { name: "paralith", version: VERSION },
2046
- { instructions: READ_ONLY ? `${READ_ONLY_NOTE}
2047
-
2048
- ${INSTRUCTIONS}` : INSTRUCTIONS }
2200
+ { instructions: READ_ONLY ? READ_ONLY_INSTRUCTIONS : INSTRUCTIONS }
2049
2201
  );
2050
2202
  tool(
2051
2203
  server,
@@ -2072,7 +2224,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2072
2224
  tool(
2073
2225
  server,
2074
2226
  "read_state",
2075
- `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.`,
2227
+ `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.`,
2076
2228
  { process: z9.string(), key: z9.string() },
2077
2229
  async ({ process: process2, key }) => readKey(process2, key),
2078
2230
  READ
@@ -2080,7 +2232,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2080
2232
  tool(
2081
2233
  server,
2082
2234
  "send_message",
2083
- "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.",
2235
+ "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.",
2084
2236
  {
2085
2237
  process: z9.string(),
2086
2238
  tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
@@ -2088,7 +2240,8 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2088
2240
  push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
2089
2241
  },
2090
2242
  async ({ process: process2, tags: tags2, data, push_outbox }) => {
2091
- if (Object.keys(tags2).some((k) => k.toLowerCase() === "target")) throw new Error("Target is not a usable tag name.");
2243
+ const reserved = Object.keys(tags2).find((k) => ["target", "signature", "epoch"].includes(k.toLowerCase().replace(/[-_]/g, "")));
2244
+ if (reserved) throw new Error(`${reserved} is not a usable tag name (Target, Signature and Epoch are reserved).`);
2092
2245
  const { api } = await runtime();
2093
2246
  return api.rawSend(
2094
2247
  Object.entries(tags2).map(([name, value]) => ({ name, value })),
package/dist/server.mjs CHANGED
@@ -506,6 +506,65 @@ var init_runtime = __esm({
506
506
  }
507
507
  });
508
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
+
509
568
  // src/update.mjs
510
569
  function check(now) {
511
570
  checkedAt = now;
@@ -523,7 +582,8 @@ function updateNote(now = Date.now()) {
523
582
  var VERSION, CHECK_MS, NOTE_MS, latest, checkedAt, notedAt, newer;
524
583
  var init_update = __esm({
525
584
  "src/update.mjs"() {
526
- VERSION = "0.4.2";
585
+ init_package();
586
+ VERSION = package_default.version;
527
587
  CHECK_MS = 6 * 36e5;
528
588
  NOTE_MS = 36e5;
529
589
  latest = null;
@@ -651,13 +711,20 @@ function side(s) {
651
711
  freeMovesUsed: Object.keys(s.freeUsed ?? {})
652
712
  };
653
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
+ }
654
720
  function battle(b, address) {
655
721
  if (!b || typeof b !== "object") return b;
656
- const mine = b.accepterAddress === address ? "accepter" : "challenger";
722
+ const mine = sideOf(b, address);
657
723
  const theirs = mine === "accepter" ? "challenger" : "accepter";
724
+ const label = (s) => !mine ? s : s === mine ? "you" : "opponent";
658
725
  const turns = (b.turns ?? []).filter((t) => t.round === b.round - 1 || t.round === b.round).map((t) => ({
659
726
  round: t.round,
660
- by: t.attacker === mine ? "you" : "opponent",
727
+ by: label(t.attacker),
661
728
  move: t.move,
662
729
  damage: t.healthDamage,
663
730
  shieldDamage: t.shieldDamage,
@@ -666,6 +733,7 @@ function battle(b, address) {
666
733
  superEffective: t.superEffective,
667
734
  notEffective: t.notEffective
668
735
  }));
736
+ const sides = mine ? { you: side(b[mine]), opponent: side(b[theirs]) } : { challenger: side(b.challenger), accepter: side(b.accepter) };
669
737
  return {
670
738
  id: b.id,
671
739
  kind: b.kind,
@@ -673,8 +741,7 @@ function battle(b, address) {
673
741
  round: b.round,
674
742
  ...b.winner ? { winner: b.winner } : {},
675
743
  ...b.waitingOn?.length ? { waitingOn: b.waitingOn } : {},
676
- you: side(b[mine]),
677
- opponent: side(b[theirs]),
744
+ ...sides,
678
745
  recentTurns: turns
679
746
  };
680
747
  }
@@ -702,18 +769,25 @@ async function me() {
702
769
  const { api, wallets } = await runtime();
703
770
  return { api, address: wallets?.ao.address };
704
771
  }
705
- async function player2() {
706
- const { api, address } = await me();
707
- if (!address) throw new Error("This server has no wallet; pass an address or battle_id.");
708
- const p = await api.readPlayer(address);
709
- 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.");
710
778
  return p;
711
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
+ }
712
786
  function registerGame(server) {
713
787
  tool(
714
788
  server,
715
789
  "game_player",
716
- "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.",
717
791
  { address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
718
792
  async ({ address }) => {
719
793
  const { api, address: mine } = await me();
@@ -729,7 +803,13 @@ function registerGame(server) {
729
803
  "game_catalog",
730
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.",
731
805
  {},
732
- 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
+ },
733
813
  READ
734
814
  );
735
815
  tool(
@@ -751,7 +831,11 @@ function registerGame(server) {
751
831
  factions: factions?.map(({ members, ...f }) => f),
752
832
  arenaTiers: tiers,
753
833
  openChallenges: challenges,
754
- 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
+ })
755
839
  };
756
840
  },
757
841
  READ
@@ -768,7 +852,7 @@ function registerGame(server) {
768
852
  tool(
769
853
  server,
770
854
  "game_companion",
771
- "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.",
772
856
  {
773
857
  action: z2.enum(["feed", "play", "quest", "claim", "adopt", "store", "retrieve", "set_active", "transfer"]),
774
858
  monster_id: z2.string().optional().describe("Companion id; omit for the active one."),
@@ -804,7 +888,7 @@ function registerGame(server) {
804
888
  tool(
805
889
  server,
806
890
  "game_level_up",
807
- "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.",
808
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) },
809
893
  async (points) => (await me()).api.levelUp(points),
810
894
  WRITE,
@@ -813,7 +897,7 @@ function registerGame(server) {
813
897
  tool(
814
898
  server,
815
899
  "game_learn_move",
816
- "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.",
817
901
  { move: z2.string().describe("The offered move, exactly as in the player record."), replace: z2.string().optional() },
818
902
  async ({ move, replace }) => (await me()).api.learnMove(move, replace),
819
903
  WRITE,
@@ -837,27 +921,31 @@ function registerGame(server) {
837
921
  WRITE,
838
922
  asPlayer
839
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
+ );
840
932
  tool(
841
933
  server,
842
934
  "game_shop",
843
- "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.",
844
936
  {
845
- action: z2.enum(["quote", "buy", "sell"]),
846
- item: z2.string().optional().describe("Item id, e.g. fire_berry or scroll."),
847
- quantity: z2.number().int().positive().optional(),
937
+ action: z2.enum(["buy", "sell"]),
938
+ item: z2.enum(SHOP_ITEMS),
939
+ quantity: z2.number().int().positive(),
848
940
  max_total: z2.number().int().optional(),
849
941
  min_total: z2.number().int().optional()
850
942
  },
851
- async ({ action, item, quantity, max_total, min_total }) => {
852
- const { api } = await me();
853
- if (action === "quote") return api.readEconomy();
854
- return api.tradeGameShop(
855
- action,
856
- need(item, "item"),
857
- need(quantity, "quantity"),
858
- { maxTotal: max_total, minTotal: min_total }
859
- );
860
- },
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
+ ),
861
949
  WRITE,
862
950
  asPlayer
863
951
  );
@@ -903,13 +991,23 @@ function registerGame(server) {
903
991
  tool(
904
992
  server,
905
993
  "arena_battle",
906
- "Read the current battle: both sides' health, moves with uses left, round number and log. Free. Defaults to your live battle.",
907
- { battle_id: z2.string().optional() },
908
- async ({ battle_id }) => {
909
- const { api } = await me();
910
- const id = battle_id ?? (await player2()).battleFleet?.battleId;
911
- if (!id) return { battle: null, note: "No live battle. Use arena_fight to start one." };
912
- 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);
913
1011
  },
914
1012
  READ,
915
1013
  battleView
@@ -947,7 +1045,7 @@ function registerGame(server) {
947
1045
  tool(
948
1046
  server,
949
1047
  "hunt_begin",
950
- "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.",
951
1049
  { monster_id: z2.string().describe("Companion to hunt with (see game_player).") },
952
1050
  async ({ monster_id }) => (await me()).api.beginHunt(monster_id),
953
1051
  WRITE,
@@ -956,14 +1054,14 @@ function registerGame(server) {
956
1054
  tool(
957
1055
  server,
958
1056
  "hunt_state",
959
- "Read your live hunt run: encounter, wild creature, round, status. Free.",
960
- {},
961
- 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 }) => {
962
1060
  const { api } = await me();
963
- const route = (await player2()).hunt;
964
- 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." };
965
1063
  },
966
- OWN,
1064
+ READ,
967
1065
  huntView
968
1066
  );
969
1067
  tool(
@@ -998,12 +1096,24 @@ function registerGame(server) {
998
1096
  WRITE,
999
1097
  huntView
1000
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
+ );
1001
1111
  tool(
1002
1112
  server,
1003
1113
  "companion_market",
1004
- "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.",
1005
1115
  {
1006
- action: z2.enum(["browse", "list", "buy", "cancel"]),
1116
+ action: z2.enum(["list", "buy", "cancel"]),
1007
1117
  monster_id: z2.string().optional(),
1008
1118
  price: z2.number().int().min(1).max(1e6).optional(),
1009
1119
  listing_id: z2.string().optional()
@@ -1011,8 +1121,6 @@ function registerGame(server) {
1011
1121
  async ({ action, monster_id, price, listing_id }) => {
1012
1122
  const { api } = await me();
1013
1123
  switch (action) {
1014
- case "browse":
1015
- return { listings: await api.readMarket(), history: await api.readMarketHistory() };
1016
1124
  case "list":
1017
1125
  return api.listMonster(need(monster_id, "monster_id"), need(price, "price"));
1018
1126
  case "buy":
@@ -1040,7 +1148,7 @@ function registerGame(server) {
1040
1148
  tool(
1041
1149
  server,
1042
1150
  "element_claim",
1043
- "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.",
1044
1152
  {},
1045
1153
  async () => {
1046
1154
  const { api, address } = await me();
@@ -1050,9 +1158,6 @@ function registerGame(server) {
1050
1158
  WRITE
1051
1159
  );
1052
1160
  }
1053
- async function battleView(b) {
1054
- return b?.id ? battle(b, (await me()).address) : b;
1055
- }
1056
1161
  async function attackView(p) {
1057
1162
  if (!p?.address) return p;
1058
1163
  const { address, gold, battlesRemaining, wins, losses, arenaLast } = p;
@@ -1069,7 +1174,7 @@ function need(value, name) {
1069
1174
  if (value === void 0 || value === null || value === "") throw new Error(`${name} is required for this action`);
1070
1175
  return value;
1071
1176
  }
1072
- var FACTIONS, BERRIES, asPlayer, huntView;
1177
+ var FACTIONS, BERRIES, SHOP_ITEMS, seenBy, asPlayer, battleView, huntView;
1073
1178
  var init_game = __esm({
1074
1179
  "src/game.mjs"() {
1075
1180
  init_runtime();
@@ -1077,7 +1182,10 @@ var init_game = __esm({
1077
1182
  init_views();
1078
1183
  FACTIONS = ["Sky Nomads", "Aqua Guardians", "Inferno Blades", "Stone Titans"];
1079
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;
1080
1187
  asPlayer = (r) => r?.address && r?.inventory ? player(r) : r;
1188
+ battleView = (b) => b?.id ? battle(b, b.viewer) : b;
1081
1189
  huntView = (r) => r?.runId ? hunt(r) : asPlayer(r);
1082
1190
  }
1083
1191
  });
@@ -1102,9 +1210,14 @@ function toWire(t, m, price, quantity) {
1102
1210
  const baseAtoms = parseAmount(quantity, baseDen);
1103
1211
  if (baseAtoms % lot !== 0n) throw new Error(`Quantity moves in steps of ${formatAtoms(lot, baseDen)} ${m.base}.`);
1104
1212
  const perUnit = parseAmount(price, t.den(m.quote));
1105
- const perLot = perUnit * lot / 10n ** BigInt(baseDen);
1106
- if (perLot <= 0n) throw new Error("Price is below one quote atom per lot.");
1107
- 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() };
1108
1221
  }
1109
1222
  function registerExchange(server) {
1110
1223
  tool(
@@ -1129,22 +1242,21 @@ function registerExchange(server) {
1129
1242
  const { api } = await runtime();
1130
1243
  const book = (await api.readVenueBook(t.process))?.[m.id];
1131
1244
  if (!book) return { market: m.id, book: null };
1132
- const unitPrice = (perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
1133
- const units = (lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
1134
- 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 });
1135
1247
  return {
1136
1248
  market: m.id,
1137
1249
  units: `price in ${m.quote} per 1 ${m.base}; quantity in ${m.base}`,
1138
- bestBid: book.bestBid ? unitPrice(book.bestBid) : null,
1139
- bestAsk: book.bestAsk ? unitPrice(book.bestAsk) : null,
1250
+ bestBid: book.bestBid ? price(book.bestBid) : null,
1251
+ bestAsk: book.bestAsk ? price(book.bestAsk) : null,
1140
1252
  bids: (book.depth?.bids ?? []).slice(0, 10).map(level),
1141
1253
  asks: (book.depth?.asks ?? []).slice(0, 10).map(level),
1142
- 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) },
1143
1255
  today: book.candles?.at(-1) && {
1144
- open: unitPrice(book.candles.at(-1).o),
1145
- high: unitPrice(book.candles.at(-1).h),
1146
- low: unitPrice(book.candles.at(-1).l),
1147
- 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),
1148
1260
  trades: book.candles.at(-1).n
1149
1261
  }
1150
1262
  };
@@ -1154,7 +1266,7 @@ function registerExchange(server) {
1154
1266
  tool(
1155
1267
  server,
1156
1268
  "exchange_position",
1157
- "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.",
1158
1270
  { venue: VENUE, address: z3.string().optional() },
1159
1271
  async ({ venue, address }) => {
1160
1272
  const t = await terms(venue);
@@ -1162,7 +1274,33 @@ function registerExchange(server) {
1162
1274
  const who = address ?? wallets?.ao.address;
1163
1275
  if (!who) throw new Error("This server has no wallet; pass an address.");
1164
1276
  const p = await api.readVenuePosition(t.process, who);
1165
- 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
+ };
1166
1304
  },
1167
1305
  READ
1168
1306
  );
@@ -1260,13 +1398,15 @@ function registerExchange(server) {
1260
1398
  WRITE
1261
1399
  );
1262
1400
  }
1263
- var VENUE, venueProcess, human;
1401
+ var VENUE, venueProcess, unitPrice, units, human;
1264
1402
  var init_exchange = __esm({
1265
1403
  "src/exchange.mjs"() {
1266
1404
  init_runtime();
1267
1405
  init_tool();
1268
1406
  VENUE = z3.enum(["game", "token"]).describe("game: items vs Gold. token: element tokens vs TEST-RUNE.");
1269
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));
1270
1410
  human = (t, free) => Object.fromEntries(Object.entries(free ?? {}).map(([asset, atoms]) => [asset, formatAtoms(atoms, t.den(asset))]));
1271
1411
  }
1272
1412
  });
@@ -2279,9 +2419,7 @@ import { z as z9 } from "zod";
2279
2419
  function buildServer() {
2280
2420
  const server = new McpServer(
2281
2421
  { name: "paralith", version: VERSION },
2282
- { instructions: READ_ONLY ? `${READ_ONLY_NOTE}
2283
-
2284
- ${INSTRUCTIONS}` : INSTRUCTIONS }
2422
+ { instructions: READ_ONLY ? READ_ONLY_INSTRUCTIONS : INSTRUCTIONS }
2285
2423
  );
2286
2424
  tool(
2287
2425
  server,
@@ -2308,7 +2446,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2308
2446
  tool(
2309
2447
  server,
2310
2448
  "read_state",
2311
- `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.`,
2312
2450
  { process: z9.string(), key: z9.string() },
2313
2451
  async ({ process: process2, key }) => readKey(process2, key),
2314
2452
  READ
@@ -2316,7 +2454,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2316
2454
  tool(
2317
2455
  server,
2318
2456
  "send_message",
2319
- "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.",
2320
2458
  {
2321
2459
  process: z9.string(),
2322
2460
  tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
@@ -2324,7 +2462,8 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2324
2462
  push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
2325
2463
  },
2326
2464
  async ({ process: process2, tags: tags2, data, push_outbox }) => {
2327
- 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).`);
2328
2467
  const { api } = await runtime();
2329
2468
  return api.rawSend(
2330
2469
  Object.entries(tags2).map(([name, value]) => ({ name, value })),
@@ -2354,7 +2493,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
2354
2493
  registerProgress(server);
2355
2494
  return server;
2356
2495
  }
2357
- var DOCS, INSTRUCTIONS, READ_ONLY_NOTE;
2496
+ var DOCS, INSTRUCTIONS, READ_ONLY_INSTRUCTIONS;
2358
2497
  var init_build = __esm({
2359
2498
  "src/build.mjs"() {
2360
2499
  init_runtime();
@@ -2373,29 +2512,33 @@ var init_build = __esm({
2373
2512
  Everything is TEST-prefixed while unreleased. Reads are free; every write is a signed message (a few seconds round trip, longer under load).
2374
2513
 
2375
2514
  Start: wallet -> game_player. No account? game_join (pick a faction).
2376
- Core loop: game_daily once a day; game_companion feed/play/quest/claim to keep energy and happiness up and earn exp and Gold;
2377
- 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).
2378
2517
  Arena: arena_enter (25 energy + 25 happiness, 4 fights), arena_fight mode=bot, then arena_attack each round
2379
2518
  (read arena_battle for moves and uses left; Rally and Mend are free once each), arena_end leave when done.
2380
- 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.
2381
2521
  Markets: exchange_markets / exchange_book / exchange_order on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE).
2382
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.
2383
2523
  Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
2384
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.
2385
2525
 
2386
2526
  HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
2387
- - Gold (in-game only, never leaves the game). Earn: companion play/quest/claim (capped at 60 Gold per 20 h), arena bot wins,
2388
- 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.
2389
2530
  - Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
2390
- 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.
2391
2532
  - Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
2392
- Get Rune: (1) the shop, only if game_shop quote shows an ask (it often only bids); (2) sell a companion on companion_market;
2393
- (3) the token route, which always works: element_claim (free daily, 1-10 of one element) -> exchange_order venue "token"
2394
- sell that element for TEST-RUNE -> rune_move deposit. Public TEST accounts accrue no Rune from game_daily (pass holders only).
2395
- 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.
2396
2539
  - Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
2397
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).
2398
- 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).
2399
2542
  - Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
2400
2543
 
2401
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.
@@ -2406,8 +2549,24 @@ Show once per thing, not once per round.
2406
2549
 
2407
2550
  Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
2408
2551
  A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
2409
- READ_ONLY_NOTE = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address.
2410
- 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.`;
2411
2570
  }
2412
2571
  });
2413
2572
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paralith-mcp",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "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).",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://docs.paralithmarkets.com/ai/mcp",