diff --git a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs
new file mode 100644
index 0000000..7735d8f
--- /dev/null
+++ b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs
@@ -0,0 +1,71 @@
+using SVSim.BattleNode.Protocol.Bodies;
+
+namespace SVSim.BattleNode.Sessions.Dispatch;
+
+/// Pure transforms from the active player's RawBody sub-structures to the opponent-facing
+/// shapes. No session state, no wire I/O — unit-testable in isolation. RawBody nested values arrive
+/// as Dictionary<string,object?> / List<object?> with numeric leaves boxed
+/// as long/int/double (see MsgEnvelope.FromJson).
+internal static class KnownListBuilder
+{
+ /// The played card's knownList entry, or null when its identity can't be synthesized
+ /// (token idx not in the deck map, or no matching move op). spellboost/attachTarget default to
+ /// 0/"" for the vanilla slice; cost/clan/tribe are deferred (receiver re-derives from cardId).
+ public static KnownCardEntry? BuildPlayedCard(
+ IReadOnlyDictionary deckMap, int playIdx, object? orderList)
+ {
+ if (!deckMap.TryGetValue(playIdx, out var cardId)) return null;
+ var to = ExtractMoveTo(orderList, playIdx);
+ if (to is null) return null;
+ return new KnownCardEntry(Idx: playIdx, CardId: cardId, To: to.Value, Spellboost: 0, AttachTarget: "");
+ }
+
+ /// The to place-state of the move op whose idx list contains
+ /// , or null if absent.
+ public static int? ExtractMoveTo(object? orderList, int playIdx)
+ {
+ if (orderList is not IEnumerable