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/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 })),