diff --git a/src/NATS.Server/Gateways/GatewayCommands.cs b/src/NATS.Server/Gateways/GatewayCommands.cs
new file mode 100644
index 0000000..394d5e9
--- /dev/null
+++ b/src/NATS.Server/Gateways/GatewayCommands.cs
@@ -0,0 +1,87 @@
+using System.Text;
+
+namespace NATS.Server.Gateways;
+
+///
+/// Gateway wire protocol command types matching Go's gateway.go format.
+/// Go reference: gateway.go:90-120 — gateway protocol constants.
+///
+public enum GatewayCommandType
+{
+ /// Gateway connection info exchange.
+ Info,
+ /// Subscribe interest notification.
+ Sub,
+ /// Unsubscribe interest notification.
+ Unsub,
+ /// Account interest mode change.
+ Mode,
+ /// Routed message through gateway.
+ Msg,
+ /// Ping keepalive.
+ Ping,
+ /// Pong keepalive response.
+ Pong,
+}
+
+///
+/// Gateway wire protocol byte sequences and formatting helpers matching Go's gateway protocol.
+/// Go reference: gateway.go:90-120 — gateway protocol constants and command formatting.
+///
+public static class GatewayCommands
+{
+ // Wire format byte sequences matching Go's gateway protocol
+ public static readonly byte[] InfoPrefix = "GINFO "u8.ToArray();
+ public static readonly byte[] SubPrefix = "GS+ "u8.ToArray();
+ public static readonly byte[] UnsubPrefix = "GS- "u8.ToArray();
+ public static readonly byte[] ModePrefix = "GMODE "u8.ToArray();
+ public static readonly byte[] MsgPrefix = "GMSG "u8.ToArray();
+ public static readonly byte[] Ping = "GPING\r\n"u8.ToArray();
+ public static readonly byte[] Pong = "GPONG\r\n"u8.ToArray();
+ public static readonly byte[] Crlf = "\r\n"u8.ToArray();
+
+ ///
+ /// Formats a gateway subscribe command.
+ /// Wire format: GS+ {account} {subject}\r\n
+ /// Go reference: gateway.go — sendGatewaySubsToGateway, RS+ propagation.
+ ///
+ public static byte[] FormatSub(string account, string subject)
+ => Encoding.UTF8.GetBytes($"GS+ {account} {subject}\r\n");
+
+ ///
+ /// Formats a gateway unsubscribe command.
+ /// Wire format: GS- {account} {subject}\r\n
+ /// Go reference: gateway.go — sendGatewayUnsubToGateway, RS- propagation.
+ ///
+ public static byte[] FormatUnsub(string account, string subject)
+ => Encoding.UTF8.GetBytes($"GS- {account} {subject}\r\n");
+
+ ///
+ /// Formats a gateway mode change command.
+ /// Wire format: GMODE {account} {mode}\r\n
+ /// Mode: "O" for Optimistic (send everything), "I" for Interest-only.
+ /// Go reference: gateway.go — switchAccountToInterestMode, GMODE command.
+ ///
+ public static byte[] FormatMode(string account, GatewayInterestMode mode)
+ {
+ var modeStr = mode == GatewayInterestMode.InterestOnly ? "I" : "O";
+ return Encoding.UTF8.GetBytes($"GMODE {account} {modeStr}\r\n");
+ }
+
+ ///
+ /// Parses a gateway command type from the first bytes of a line.
+ /// Returns null if the command prefix is unrecognized.
+ /// Go reference: gateway.go — processGatewayMsg command dispatch.
+ ///
+ public static GatewayCommandType? ParseCommandType(ReadOnlySpan line)
+ {
+ if (line.StartsWith(InfoPrefix)) return GatewayCommandType.Info;
+ if (line.StartsWith(SubPrefix)) return GatewayCommandType.Sub;
+ if (line.StartsWith(UnsubPrefix)) return GatewayCommandType.Unsub;
+ if (line.StartsWith(ModePrefix)) return GatewayCommandType.Mode;
+ if (line.StartsWith(MsgPrefix)) return GatewayCommandType.Msg;
+ if (line.StartsWith(Ping)) return GatewayCommandType.Ping;
+ if (line.StartsWith(Pong)) return GatewayCommandType.Pong;
+ return null;
+ }
+}
diff --git a/tests/NATS.Server.Tests/Gateways/GatewayCommandTests.cs b/tests/NATS.Server.Tests/Gateways/GatewayCommandTests.cs
new file mode 100644
index 0000000..1a03e73
--- /dev/null
+++ b/tests/NATS.Server.Tests/Gateways/GatewayCommandTests.cs
@@ -0,0 +1,99 @@
+using System.Text;
+using NATS.Server.Gateways;
+using Shouldly;
+
+namespace NATS.Server.Tests.Gateways;
+
+// Go reference: gateway.go:90-120 — gateway protocol constants and command formatting.
+
+public class GatewayCommandTests
+{
+ [Fact]
+ public void FormatSub_produces_correct_wire_format()
+ {
+ // Go reference: gateway.go — RS+ propagation, sendGatewaySubsToGateway
+ var bytes = GatewayCommands.FormatSub("$G", "orders.>");
+ var line = Encoding.UTF8.GetString(bytes);
+ line.ShouldBe("GS+ $G orders.>\r\n");
+ }
+
+ [Fact]
+ public void FormatUnsub_produces_correct_wire_format()
+ {
+ // Go reference: gateway.go — RS- propagation, sendGatewayUnsubToGateway
+ var bytes = GatewayCommands.FormatUnsub("$G", "orders.>");
+ var line = Encoding.UTF8.GetString(bytes);
+ line.ShouldBe("GS- $G orders.>\r\n");
+ }
+
+ [Fact]
+ public void FormatMode_optimistic_produces_O()
+ {
+ // Go reference: gateway.go — GMODE command, "O" = Optimistic mode
+ var bytes = GatewayCommands.FormatMode("$G", GatewayInterestMode.Optimistic);
+ var line = Encoding.UTF8.GetString(bytes);
+ line.ShouldBe("GMODE $G O\r\n");
+ }
+
+ [Fact]
+ public void FormatMode_interest_only_produces_I()
+ {
+ // Go reference: gateway.go — GMODE command, "I" = InterestOnly mode
+ var bytes = GatewayCommands.FormatMode("$G", GatewayInterestMode.InterestOnly);
+ var line = Encoding.UTF8.GetString(bytes);
+ line.ShouldBe("GMODE $G I\r\n");
+ }
+
+ [Fact]
+ public void ParseCommandType_identifies_sub()
+ {
+ // Go reference: gateway.go — processGatewayMsg dispatch on GS+
+ var line = Encoding.UTF8.GetBytes("GS+ ACC foo.bar\r\n");
+ var result = GatewayCommands.ParseCommandType(line);
+ result.ShouldBe(GatewayCommandType.Sub);
+ }
+
+ [Fact]
+ public void ParseCommandType_identifies_unsub()
+ {
+ // Go reference: gateway.go — processGatewayMsg dispatch on GS-
+ var line = Encoding.UTF8.GetBytes("GS- ACC foo.bar\r\n");
+ var result = GatewayCommands.ParseCommandType(line);
+ result.ShouldBe(GatewayCommandType.Unsub);
+ }
+
+ [Fact]
+ public void ParseCommandType_identifies_ping()
+ {
+ // Go reference: gateway.go — keepalive GPING command
+ var result = GatewayCommands.ParseCommandType(GatewayCommands.Ping);
+ result.ShouldBe(GatewayCommandType.Ping);
+ }
+
+ [Fact]
+ public void ParseCommandType_identifies_pong()
+ {
+ // Go reference: gateway.go — keepalive GPONG response
+ var result = GatewayCommands.ParseCommandType(GatewayCommands.Pong);
+ result.ShouldBe(GatewayCommandType.Pong);
+ }
+
+ [Fact]
+ public void ParseCommandType_returns_null_for_unknown()
+ {
+ // Unrecognized commands should return null for graceful handling
+ var line = Encoding.UTF8.GetBytes("UNKNOWN something\r\n");
+ var result = GatewayCommands.ParseCommandType(line);
+ result.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Wire_format_constants_end_correctly()
+ {
+ // Go reference: gateway.go — all gateway commands use CRLF line endings
+ var crlf = new byte[] { (byte)'\r', (byte)'\n' };
+ GatewayCommands.Ping.TakeLast(2).ShouldBe(crlf);
+ GatewayCommands.Pong.TakeLast(2).ShouldBe(crlf);
+ GatewayCommands.Crlf.ShouldBe(crlf);
+ }
+}