diff --git a/.idea/.idea.Blacklight/.idea/avalonia.xml b/.idea/.idea.Blacklight/.idea/avalonia.xml
index f8515eba138f299b94761bc1e7faae9151455ef6..6dab72c4346600c9550bce9c1c32463e1940ebbb 100644
--- a/.idea/.idea.Blacklight/.idea/avalonia.xml
+++ b/.idea/.idea.Blacklight/.idea/avalonia.xml
@@ -20,7 +20,7 @@
         <entry key="Blacklight/Views/MainView.axaml" value="Blacklight/Blacklight.csproj" />
         <entry key="Blacklight/Views/MainWindow.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/Tools/BeltRight.axaml" value="Blacklight/Blacklight.csproj" />
-        <entry key="Blacklight/Views/Tools/ResourceExplorerView.axaml" value="Blacklight/Blacklight.csproj" />
+        <entry key="Blacklight/Views/Tools/ResourceExplorerView.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/Utility/MarkdownText.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/Views/ClientView.axaml" value="Blacklight/Blacklight.csproj" />
         <entry key="Blacklight/Views/Views/SettingsView.axaml" value="Blacklight/Blacklight.csproj" />
diff --git a/Blacklight/App.axaml.cs b/Blacklight/App.axaml.cs
index 06f9727bc02a2cc8da5346066ae7ba54da7711ae..cb7d2e5471e976f8639956e696babf9462d6e2ab 100644
--- a/Blacklight/App.axaml.cs
+++ b/Blacklight/App.axaml.cs
@@ -11,6 +11,7 @@ using Blacklight.Views;
 using Blacklight.Views.Login;
 using Lightquark.NET;
 using Microsoft.Extensions.DependencyInjection;
+using Serilog;
 
 namespace Blacklight;
 
@@ -26,10 +27,18 @@ public partial class App : Application
 
     public override void OnFrameworkInitializationCompleted()
     {
+        Log.Logger = new LoggerConfiguration()
+            .MinimumLevel.Debug()
+            .WriteTo.Console()
+            .WriteTo.File(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "blacklight.log"), 
+                rollingInterval: RollingInterval.Day, 
+                fileSizeLimitBytes: 2_000_000)
+            .CreateLogger();
         if (Services == null)
         {
             var services = new ServiceCollection();
 
+            services.AddSingleton(Log.Logger);
             services.AddSingleton<MainWindowViewModel>();
             services.AddSingleton<ViewNavigationService>();
             services.AddSingleton<Client>();
@@ -51,7 +60,7 @@ public partial class App : Application
                 var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
                 try
                 {
-                    Console.WriteLine("Deleting login.json (1)");
+                    Log.Debug("Deleting login.json (1)");
                     File.Delete(path);
                 }
                 catch (Exception ex)
@@ -62,7 +71,7 @@ public partial class App : Application
         }
         else
         {
-            Console.Error.WriteLine("Services not null when entering OnFrameworkInitializationCompleted");
+            Log.Error("Services not null when entering OnFrameworkInitializationCompleted");
         }
 
         var mainWindowViewModel = Services.GetRequiredService<MainWindowViewModel>();
diff --git a/Blacklight/Blacklight.csproj b/Blacklight/Blacklight.csproj
index 8e96b249df97cc95269cc5b0919a2c73892fbfbd..44b879eb95e16045143269a8a1797023956b8507 100644
--- a/Blacklight/Blacklight.csproj
+++ b/Blacklight/Blacklight.csproj
@@ -25,6 +25,9 @@
         <PackageReference Include="Markdig" Version="0.37.0" />
         <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
         <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+        <PackageReference Include="Serilog" Version="4.0.1" />
+        <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
+        <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
     </ItemGroup>
 
 
diff --git a/Blacklight/Util/ViewNavigationService.cs b/Blacklight/Util/ViewNavigationService.cs
index 380401c1afaf5838c2a0abb6488645f4f9108bd6..df27a00d31e6de1071a2958d17d8505c9cd11c5d 100644
--- a/Blacklight/Util/ViewNavigationService.cs
+++ b/Blacklight/Util/ViewNavigationService.cs
@@ -1,6 +1,7 @@
 using System;
 using Blacklight.ViewModels;
 using Microsoft.Extensions.DependencyInjection;
+using Serilog;
 
 namespace Blacklight.Util;
 
@@ -11,7 +12,7 @@ public class ViewNavigationService
 
     public ViewNavigationService(IServiceProvider serviceProvider)
     {
-        Console.WriteLine($"Nav init!");
+        Log.Debug($"Nav init!");
         _serviceProvider = serviceProvider;
     }
     
diff --git a/Blacklight/ViewModels/LoginViewModel.cs b/Blacklight/ViewModels/LoginViewModel.cs
index ea9fc3c7aaa806a3b800d7e7beb7472fa7061ce2..108a2b3bf93ad52d73a7c340a709544e58ff54eb 100644
--- a/Blacklight/ViewModels/LoginViewModel.cs
+++ b/Blacklight/ViewModels/LoginViewModel.cs
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
 using Lightquark.NET;
 using Lightquark.Types;
 using Newtonsoft.Json;
+using Serilog;
 
 namespace Blacklight.ViewModels;
 
@@ -85,19 +86,19 @@ public class LoginViewModel : ViewModelBase
         {
             try
             {
-                Console.WriteLine("Found login.json, loading existing data");
+                Log.Debug("Found login.json, loading existing data");
                 var json = File.ReadAllText(path);
                 var persistentData = JsonConvert.DeserializeObject<LoginViewModelPersistent>(json);
                 LoadExistingData(persistentData!);
             }
             catch (Exception ex)
             {
-                Console.Error.WriteLine(ex);
+                Log.Error(ex, "Failed to load login.json");
             }
         }
         else
         {
-            Console.WriteLine("No login.json");
+            Log.Debug("No login.json");
             SwitchNetwork();
             _currentControl = new MainLoginPrompt();
         }
@@ -108,16 +109,17 @@ public class LoginViewModel : ViewModelBase
         try
         {
             
-            Console.WriteLine($"Connecting to {persistentData.NetworkBase} from login.json");
+            Log.Debug("Connecting to {PersistentDataNetworkBase} from login.json", persistentData.NetworkBase);
             LoadingText = "Connecting to network";
             Network = persistentData.NetworkBase;
             var network = await Client.GetNetworkAsync(new Uri(persistentData.NetworkBase));
-            Console.WriteLine($"Fetch successful? {network.success} ({network.error})");
+            Log.Debug("Fetch successful? {NetworkSuccess} ({NetworkError})", network.success, network.error);
             if (network.success)
             {
+                LoadingText = "Signing in";
                 Client.NetworkInformation = network.networkInformation;
                 Client.UseToken(persistentData.AccessToken, persistentData.RefreshToken);
-                Console.WriteLine("Found token, showing ClientView");
+                Log.Debug("Found token, showing ClientView");
                 EnterClient();
             }
             else
@@ -128,7 +130,7 @@ public class LoginViewModel : ViewModelBase
         }
         catch (UriFormatException ex)
         {
-            Console.Error.WriteLine(ex);
+            Log.Error(ex, "Failed to load network");
             LoadingText = $"{ex.Message}";
             LoadingError = true;
         }
@@ -168,12 +170,12 @@ public class LoginViewModel : ViewModelBase
         var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
         try
         {
-            Console.WriteLine("Deleting login.json (2)");
+            Log.Debug("Deleting login.json (2)");
             File.Delete(path);
         }
         catch (Exception ex)
         {
-            Console.Error.WriteLine(ex);
+            Log.Error(ex, "Failed to delete login.json");
         }
         UseSolstice();
     }
@@ -193,7 +195,7 @@ public class LoginViewModel : ViewModelBase
         if (res.success)
         {
             await SavePersist(res.accessToken!, res.refreshToken!);
-            Console.WriteLine("Logged in, entering client");
+            Log.Debug("Logged in, entering client");
             EnterClient();
         }
         else
@@ -205,10 +207,12 @@ public class LoginViewModel : ViewModelBase
 
     private async void EnterClient()
     {
+        Log.Debug("EnterClient");
         LoadingText = "Waiting for gateway connection";
         LoadingError = false;
         _currentControl = new Loading();
-        Client.GatewayConnectionEvent.Wait();
+        Log.Debug("GatewayConnection Wait");
+        await Client.GatewayConnectionEvent.WaitAsync();
         LoadingText = "Getting user data";
         await Client.GetCurrentUser();
         _nav.ShowView<ClientViewModel>();
@@ -218,7 +222,6 @@ public class LoginViewModel : ViewModelBase
     
     private async Task SavePersist(string accessToken, string refreshToken)
     {
-        Console.WriteLine($"Saving token {accessToken}");
         var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
         Directory.CreateDirectory(Path.GetDirectoryName(path)!);
         await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(new LoginViewModelPersistent
@@ -227,7 +230,7 @@ public class LoginViewModel : ViewModelBase
             AccessToken = accessToken,
             RefreshToken = refreshToken
         }));
-        Console.WriteLine("Token saved!");
+        Log.Debug("Token saved!");
     }
     
     private void EnterNetworkSwitcher()
diff --git a/Blacklight/ViewModels/MainWindowViewModel.cs b/Blacklight/ViewModels/MainWindowViewModel.cs
index f25cbbcc5e2b812525109e75efa460cd3683c8cb..554c73c5b9b47f9f920bcbcea5fe764286d09b41 100644
--- a/Blacklight/ViewModels/MainWindowViewModel.cs
+++ b/Blacklight/ViewModels/MainWindowViewModel.cs
@@ -7,6 +7,7 @@ using Blacklight.Util;
 using Blacklight.Views.Login;
 using Lightquark.NET;
 using Microsoft.Extensions.DependencyInjection;
+using Serilog;
 using Path = System.IO.Path;
 
 namespace Blacklight.ViewModels;
@@ -18,7 +19,7 @@ public class MainWindowViewModel : ViewModelBase
         get => _currentViewModel;
         set
         {
-            Console.WriteLine($"Changed view model to {value.GetType().Name}");
+            Log.Debug("Changed view model to {Name}", value.GetType().Name);
             SetProperty(ref _currentViewModel, value);
         }
     }
diff --git a/Blacklight/Views/Utility/MarkdownText.axaml.cs b/Blacklight/Views/Utility/MarkdownText.axaml.cs
index 5ba74a1bb57507993ef5d66358ff747d02d59edf..dfbf9f605f60adf7be8c8dc5ad7449416278c46a 100644
--- a/Blacklight/Views/Utility/MarkdownText.axaml.cs
+++ b/Blacklight/Views/Utility/MarkdownText.axaml.cs
@@ -13,6 +13,7 @@ using Markdig;
 using Markdig.Syntax;
 using Markdig.Syntax.Inlines;
 using Microsoft.Extensions.DependencyInjection;
+using Serilog;
 using TheArtOfDev.HtmlRenderer.Avalonia;
 using TheArtOfDev.HtmlRenderer.Core.Entities;
 
@@ -57,21 +58,21 @@ public partial class MarkdownText : UserControl
             }
             else
             {
-                Console.WriteLine("Not a network! Opening link...");
+                Log.Debug("Not a network! Opening link...");
                 var topLevel = TopLevel.GetTopLevel(this);
                 if (topLevel == null)
                 {
-                    Console.WriteLine("Top level null");
+                    Log.Debug("Top level null");
                     return;
                 }
 
                 await Dispatcher.UIThread.InvokeAsync(async () =>
                 {
                     var success = await topLevel.Launcher.LaunchUriAsync(new Uri(e.Event.Link));
-                    Console.WriteLine($"Opened? {success}");
+                    Log.Debug("Opened? {Success}", success);
                     if (!success)
                     {
-                        Console.WriteLine($"Fallback method...");
+                        Log.Debug($"Fallback method...");
                         Process.Start(new ProcessStartInfo
                         {
                             FileName = e.Event.Link,
diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs
index e0d22cfda5d521f9b50e95f2a8a42c3b90e0998b..c018410b3aa49094e3e509d16f05ac568d7412f1 100644
--- a/Lightquark.NET/Client.cs
+++ b/Lightquark.NET/Client.cs
@@ -8,6 +8,7 @@ using Lightquark.NET.Util.Converters;
 using MongoDB.Bson;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Serialization;
+using Serilog;
 
 namespace Lightquark.NET;
 
@@ -20,7 +21,6 @@ public enum GatewayConnectState
 
 public partial class Client : ObservableObject
 {
-
     private const string Version = "v4";
     public ObservableCollection<ObjectId> QuarkList { get; set; } = [];
     public ObservableCollection<Quark> Quarks { get; set; } = [];
@@ -64,8 +64,9 @@ public partial class Client : ObservableObject
     private User? _currentUser;
     private IMapper _mapper;
 
-    public Client()
+    public Client(ILogger? logger = null)
     {
+        Log.Logger = logger ?? new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
         JsonConvert.DefaultSettings = () => new JsonSerializerSettings
         {
             Formatting = Formatting.Indented,
@@ -90,7 +91,7 @@ public partial class Client : ObservableObject
             var expiryTime = Base36Converter.ConvertFrom(expirationTimePart);
             var origin = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
             var expiryDate = origin.AddMilliseconds(expiryTime);
-            Console.WriteLine($"Token expires at {expiryDate}");
+            Log.Debug("Token expires at {ExpiryDate}", expiryDate);
             var refreshTime = expiryDate.Subtract(TimeSpan.FromMinutes(15));
             var timeUntilRefresh = refreshTime - DateTimeOffset.Now;
             if (timeUntilRefresh < TimeSpan.FromSeconds(1))
@@ -114,11 +115,11 @@ public partial class Client : ObservableObject
                 }
             }
         }
-        Console.WriteLine($"Detected {e.PropertyName} change");
+        Log.Debug("Detected {EPropertyName} change", e.PropertyName);
 
         if (e.PropertyName == "RefreshToken" && RefreshToken == null)
         {
-            Console.WriteLine("Detected LogOut");
+            Log.Debug("Detected LogOut");
             RunLogOut();
         }
         base.OnPropertyChanged(e);
diff --git a/Lightquark.NET/ClientMethods/Gatekeeper.cs b/Lightquark.NET/ClientMethods/Gatekeeper.cs
new file mode 100644
index 0000000000000000000000000000000000000000..19998b87a7f4e1f852ad7eadc427f33e3e9b413d
--- /dev/null
+++ b/Lightquark.NET/ClientMethods/Gatekeeper.cs
@@ -0,0 +1,198 @@
+using System.Diagnostics;
+using System.Net.WebSockets;
+using Lightquark.NET.Util;
+using Newtonsoft.Json;
+using Serilog;
+using Websocket.Client;
+
+namespace Lightquark.NET;
+
+public partial class Client
+{
+    private async void GatekeeperMeasure(MeasureGatewayMessage measureMessage)
+    {
+        _gatekeeperSession = measureMessage.SessionId;
+        // List<Task> gatewayMeasureTasks
+        List<LatencyResult> gatewayLatencies = [];
+        List<LatencyResult> appServerLatencies = [];
+        foreach (var gateway in measureMessage.Gateways)
+        {
+            List<double> latencies = [];
+            Log.Information("Measuring latency to gw {GatewayInstanceId}", gateway.InstanceId);
+            for (var i = 0; i < 5; i++)
+            {
+                var latency = await MeasureGateway(gateway.GatewayUri);
+                if (latency != null)
+                {
+                    latencies.Add(latency.Value);
+                }
+
+                Log.Information("gw {GatewayInstanceId} measurement {I}: {Latency}ms", gateway.InstanceId, i + 1, latency);
+            }
+
+            if (latencies.Count > 0)
+            {
+                var latency = MedianFinder.GetMedian(latencies.ToArray());
+                gatewayLatencies.Add(new LatencyResult
+                {
+                    InstanceId = gateway.InstanceId,
+                    Latency = latency
+                });
+                Log.Information("gw {GatewayInstanceId}: {Latency}ms", gateway.InstanceId, latency);
+            }
+            else
+            {
+                Log.Information("gw {GatewayInstanceId}: Unreachable", gateway.InstanceId);
+            }
+        }
+        foreach (var appServer in measureMessage.AppServers)
+        {
+
+            List<double> latencies = [];
+            Log.Information("Measuring latency to as {AppServerInstanceId}", appServer.InstanceId);
+            for (var i = 0; i < 5; i++)
+            {
+                var latency = await MeasureAppServer(appServer.BaseUri);
+                if (latency != null)
+                {
+                    latencies.Add(latency.Value);
+                }
+
+                Log.Information("as {AppServerInstanceId} measurement {I}: {Latency}ms", appServer.InstanceId, i + 1, latency);
+            }
+
+            if (latencies.Count > 0)
+            {
+                var latency = MedianFinder.GetMedian(latencies.ToArray());
+                appServerLatencies.Add(new LatencyResult
+                {
+                    InstanceId = appServer.InstanceId,
+                    Latency = latency
+                });
+                Log.Information("as {AppServerInstanceId}: {Latency}ms", appServer.InstanceId, latency);
+            }
+            else
+            {
+                Log.Information("as {AppServerInstanceId}: Unreachable", appServer.InstanceId);
+            }
+        }
+        
+        SendGatewayMessage(new
+        {
+            @event = "gatekeeperMeasure",
+            gateways = gatewayLatencies,
+            appServers = appServerLatencies
+        });
+    }
+
+    private async Task<double?> MeasureAppServer(Uri baseUri)
+    {
+        try
+        {
+            var timer = new Stopwatch();
+            var finalUri = new Uri(baseUri, "v4/ping");
+            timer.Start();
+            var res = await _httpClient.GetAsync(finalUri);
+            res.EnsureSuccessStatusCode();
+            timer.Stop();
+            return timer.ElapsedMilliseconds;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed AS measurement");
+            return null;
+        }
+    }
+    
+    private async Task<double?> MeasureGateway(Uri gatewayUri)
+    {
+        try
+        {
+            var sem = new SemaphoreSlim(0, 1);
+            using var tempGw = new WebsocketClient(gatewayUri);
+            double? result = null;
+            var state = Guid.NewGuid().ToString();
+            var timer = new Stopwatch();
+            tempGw.DisconnectionHappened.Subscribe(_ => { sem.Release(); });
+            tempGw.ErrorReconnectTimeout = null;
+            tempGw.ReconnectTimeout = null;
+            tempGw.ReconnectionHappened.Subscribe(_ =>
+            {
+                timer.Start();
+                SendGatewayMessage(new { @event = "ping", state }, tempGw);
+            });
+            tempGw.MessageReceived.Subscribe(msg =>
+            {
+                var message = ParseGateway<BaseGatewayMessage>(msg.Text!);
+                if (message.State != state) return;
+                timer.Stop();
+                result = timer.ElapsedMilliseconds;
+                sem.Release();
+            });
+            await tempGw.Start();
+            await sem.WaitAsync();
+            await tempGw.Stop(WebSocketCloseStatus.NormalClosure, "Latency test finished");
+            return result;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed GW measurement");
+            return null;
+        }
+    }
+
+    private async void GatekeeperSelection(SelectionGatewayMessage selectionMessage)
+    {
+        Log.Information("Using {AppServer} AppServer", selectionMessage.AppServerUri);
+        NetworkInformation!.BaseUrl = selectionMessage.AppServerUri.ToString();
+        Log.Information("Waiting for {InFlightRpcCount} RPC calls to finish", _inFlightRpcs.Count);
+        await _noRpcInFlight.Task; // Wait until all RPC have finished
+        if (selectionMessage.GatewayUri.ToString() != NetworkInformation.Gateway)
+        {
+            Log.Information("Using {Gateway} Gateway", selectionMessage.GatewayUri);
+            NetworkInformation.Gateway = selectionMessage.GatewayUri.ToString();
+            _gateway!.Url = selectionMessage.GatewayUri;
+            await _gateway.Reconnect();   
+        }
+        else
+        {
+            Log.Information("Already using correct gateway {Gateway}", selectionMessage.GatewayUri);
+        }
+    }
+}
+
+public record SelectionGatewayMessage : BaseGatewayMessage
+{
+    public required string SessionId { get; init; }
+    [JsonProperty("gateway")]
+    public required Uri GatewayUri { get; init; }
+    [JsonProperty("appServer")]
+    public required Uri AppServerUri { get; init; }
+}
+
+public record MeasureGatewayMessage : BaseGatewayMessage
+{
+    public required string SessionId { get; init; }
+    public required GatewayServer[] Gateways { get; init; }
+    public required AppServer[] AppServers { get; init; }
+}
+
+public record GatewayServer
+{
+    public required string InstanceId { get; init; }
+    [JsonProperty("gateway")]
+    public required Uri GatewayUri { get; init; }
+}
+
+public record AppServer
+{
+    public required string InstanceId { get; init; }
+    [JsonProperty("baseUrl")]
+    public required Uri BaseUri { get; init; }
+}
+
+public record LatencyResult
+{
+    public required string InstanceId { get; init; }
+    public required double Latency { get; init; }
+}
diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs
index e9baf1bfdb1ebd46733777f268a0ae408824e612..574734109538a9c729047597af5782e4791f14fc 100644
--- a/Lightquark.NET/ClientMethods/Gateway.cs
+++ b/Lightquark.NET/ClientMethods/Gateway.cs
@@ -1,6 +1,9 @@
-using Lightquark.NET.Objects;
+using System.Diagnostics;
+using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
+using Lightquark.NET.Util;
 using Newtonsoft.Json;
+using Serilog;
 using Websocket.Client;
 
 namespace Lightquark.NET;
@@ -10,28 +13,37 @@ public partial class Client
     private WebsocketClient? _gateway;
     private Timer? _heartbeatTimer;
     private string? _gatekeeperSession;
-    public ManualResetEventSlim GatewayConnectionEvent = new(false);
+    public readonly AsyncManualResetEvent GatewayConnectionEvent = new(false);
+    private volatile TaskCompletionSource<bool> _noRpcInFlight = new();
+    private List<string> _inFlightRpcs = [];
+    private string? _authStateString;
     
     private async void InitialConnectGateway()
     {
         if (NetworkInformation?.Gateway == null)
         {
-            await Console.Error.WriteLineAsync("Network gateway uri null");
+            Log.Error("Network gateway uri null");
             return;
         }
         GatewayStatus = GatewayConnectState.Connecting;
         _gateway = new WebsocketClient(new Uri(NetworkInformation.Gateway));
         _gateway.ReconnectTimeout = null;
+        _gateway.ErrorReconnectTimeout = TimeSpan.FromSeconds(5);
         _gateway.DisconnectionHappened.Subscribe(info =>
         {
+            _heartbeatTimer?.Dispose();
             GatewayConnectionEvent.Reset();
             GatewayStatus = GatewayConnectState.NotConnected;
-            Console.Error.WriteLine($"Disconnection from gateway, type {info.Type}");
+            Log.Error("Disconnection from gateway, type {InfoType} {Message}", info.Type, info.Exception);
         });
         _gateway.ReconnectionHappened.Subscribe(info =>
         {
-            Console.Error.WriteLine($"Reconnection to gateway, type {info.Type}");
-            SendGatewayMessage(new { @event = "authenticate", gatekeeperSession = _gatekeeperSession, token = AccessToken });
+            Log.Error("Reconnection to gateway, type {InfoType}", info.Type);
+            if (!_noRpcInFlight.Task.IsCompleted) _noRpcInFlight.SetResult(true); // No rpc in flight when we just connected
+            Debug.Assert(_inFlightRpcs.Count == 0, "Expected 0 in flight rpc when reconnecting");
+            _inFlightRpcs = [];
+            _authStateString = Guid.NewGuid().ToString();
+            SendGatewayMessage(new { @event = "authenticate", gatekeeperSession = _gatekeeperSession, token = AccessToken, state = _authStateString });
         });
         _gateway.MessageReceived.Subscribe(msg =>
         {
@@ -40,7 +52,7 @@ public partial class Client
                 var message = ParseGateway<BaseGatewayMessage>(msg.Text!);
                 if (message.Event != "heartbeat")
                 {
-                    Console.WriteLine(msg.Text);
+                    Log.Debug("{Text}", msg.Text);
                 }
                 EventBus.Publish(new GatewayBusEvent
                 {
@@ -51,26 +63,33 @@ public partial class Client
                 {
                     case "authenticate":
                         var authMessage = ParseGateway<AuthGatewayMessage>(msg.Text!);
-                        switch (authMessage.Code)
+                        if (authMessage.Code == 200)
                         {
-                            case 200:
+                            GatewayStatus = GatewayConnectState.Connected;
+                            GatewayConnectionEvent.Set();
+                            Log.Debug("Connected to gateway!");
+                            _heartbeatTimer?.Dispose();
+                            _heartbeatTimer = new Timer(Heartbeat, null, TimeSpan.FromSeconds(15),
+                                TimeSpan.FromSeconds(15));
+                        }
+                        else
+                        {
+                            Log.Error("Auth failure to gateway! Code {AuthMessageCode} {ObjText}", authMessage.Code, msg.Text!);
+                        }
+                        break;
+                    case "error":
+                        if (message.State == _authStateString)
+                        {
+                            var errorMessage = ParseGateway<ErrorGatewayMessage>(msg.Text!);
+                            if (errorMessage.Code == 401)
                             {
-                                GatewayStatus = GatewayConnectState.Connected;
-                                GatewayConnectionEvent.Set();
-                                Console.WriteLine("Connected to gateway!");
-                                if (_heartbeatTimer == null)
-                                {
-                                    _heartbeatTimer = new Timer(Heartbeat, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15));
-                                }
-
-                                break;
-                            }
-                            case 401:
+                                Log.Error("Failed gw auth! {ErrorMessageCode}, {ErrorMessageMessage}", errorMessage.Code, errorMessage.Message);
                                 LogOut();
-                                break;
-                            default:
-                                Console.Error.WriteLine($"Auth failure to gateway! Code {authMessage.Code}");
-                                break;
+                            }
+                        }
+                        else
+                        {
+                            Log.Error("Gw reported error!\\n{ObjText}", msg.Text!);
                         }
                         break;
                     case "rpc":
@@ -81,13 +100,20 @@ public partial class Client
                         {
                             CurrentUserFromObject(userUpdateMessage.User);
                         }
-
+                        break;
+                    case "gatekeeperMeasure":
+                        var measureMessage = ParseGateway<MeasureGatewayMessage>(msg.Text!);
+                        GatekeeperMeasure(measureMessage);
+                        break;
+                    case "gatekeeperSelection":
+                        var selectionMessage = ParseGateway<SelectionGatewayMessage>(msg.Text!);
+                        GatekeeperSelection(selectionMessage);
                         break;
                 }
             }
             catch (Exception ex)
             {
-                Console.Error.WriteLine(ex);
+                Log.Error(ex, "Failed handling incoming message");
             }
         });
 
@@ -101,42 +127,61 @@ public partial class Client
 
     public async Task<Reply<T>> CallRpc<T>(string method, string route, object? body) where T : BaseReplyResponse
     {
-        if (GatewayStatus != GatewayConnectState.Connected) throw new Exception("RPC Call when Gateway not connected!");
+        await GatewayConnectionEvent.WaitAsync(); // Wait until connected just in case
+        if (GatewayStatus != GatewayConnectState.Connected) throw new Exception("RPC Call when Gateway not connected!"); // ???
         var sem = new SemaphoreSlim(0, 1);
         var state = Guid.NewGuid().ToString();
-        RpcGatewayMessage<T>? responseBody = null;
-        var subscriptionId = EventBus.Subscribe<GatewayBusEvent>(busEvent =>
+        _inFlightRpcs.Add(state);
+        if (_noRpcInFlight.Task.IsCompleted)
         {
-            if (busEvent.BaseMessage.State != state) return;
-            responseBody = ParseGateway<RpcGatewayMessage<T>>(busEvent.RawMessageString);
-            sem.Release();
-        });
-        SendGatewayMessage(new
+            _noRpcInFlight = new TaskCompletionSource<bool>();
+        }
+        try
         {
-            @event = "rpc",
-            token = AccessToken,
-            state,
-            route,
-            method,
-            body
-        });
-        await sem.WaitAsync();
-        EventBus.Unsubscribe<GatewayBusEvent>(subscriptionId);
-        
-        return responseBody!.Body;
+            RpcGatewayMessage<T>? responseBody = null;
+            var subscriptionId = EventBus.Subscribe<GatewayBusEvent>(busEvent =>
+            {
+                if (busEvent.BaseMessage.State != state) return;
+                responseBody = ParseGateway<RpcGatewayMessage<T>>(busEvent.RawMessageString);
+                sem.Release();
+            });
+            SendGatewayMessage(new
+            {
+                @event = "rpc",
+                token = AccessToken,
+                state,
+                route,
+                method,
+                body
+            });
+            await sem.WaitAsync();
+            EventBus.Unsubscribe<GatewayBusEvent>(subscriptionId);
+            _inFlightRpcs.Remove(state);
+            if (_inFlightRpcs.Count == 0) _noRpcInFlight.SetResult(true);
+            return responseBody!.Body;
+        }
+        catch (Exception ex)
+        {
+            _inFlightRpcs.Remove(state);
+            if (_inFlightRpcs.Count == 0) _noRpcInFlight.SetResult(true);
+            Log.Error(ex, "Failed RPC call :(");
+            throw;
+        }
     }
     
-    private void SendGatewayMessage(object message)
+    private void SendGatewayMessage(object message, WebsocketClient? gateway = null)
     {
-        _gateway.Send(JsonConvert.SerializeObject(message));
+        gateway ??= _gateway;
+        gateway.Send(JsonConvert.SerializeObject(message));
     }
 
-    private T ParseGateway<T>(string message)
+    private T ParseGateway<T>(string message) where T : BaseGatewayMessage
     {
         return JsonConvert.DeserializeObject<T>(message)!;
     }
 }
 
+
 public record BaseGatewayMessage
 {
     [JsonProperty("event")]
@@ -153,6 +198,15 @@ public record AuthGatewayMessage : BaseGatewayMessage
     public required int Code { get; init; }
 }
 
+
+public record ErrorGatewayMessage : BaseGatewayMessage
+{
+    [JsonProperty("code")]
+    public required int Code { get; init; }
+    [JsonProperty("message")]
+    public required string Message { get; init; }
+}
+
 public record UserUpdateGatewayMessage : BaseGatewayMessage
 {
     [JsonProperty("user")]
diff --git a/Lightquark.NET/ClientMethods/Login.cs b/Lightquark.NET/ClientMethods/Login.cs
index 3bab6b27ec1af79ee143c5efc2d5e6215e0fa7c8..21f050254528bbbb09a184271e2687cc8426433f 100644
--- a/Lightquark.NET/ClientMethods/Login.cs
+++ b/Lightquark.NET/ClientMethods/Login.cs
@@ -2,6 +2,7 @@
 using System.Net.Http.Headers;
 using Lightquark.NET.Objects.Reply;
 using Newtonsoft.Json;
+using Serilog;
 
 namespace Lightquark.NET;
 
@@ -14,7 +15,7 @@ public partial class Client
         try
         {
             var finalUri = new Uri(new Uri(NetworkInformation!.BaseUrl!), $"{Version}/auth/token");
-            Console.WriteLine($"{finalUri}");
+            Log.Debug("{FinalUri}", finalUri);
             var res = await _httpClient.PostAsync(finalUri, new StringContent(JsonConvert.SerializeObject(new
             {
                 email, password
@@ -54,11 +55,11 @@ public partial class Client
 
     private async void AcquireToken(object? state = null)
     {
-        Console.WriteLine($"Refreshing token {AccessToken} {RefreshToken}");
+        // Log.Debug("Refreshing token {AccessToken} {RefreshToken}", AccessToken, RefreshToken);
         try
         {
             var finalUri = new Uri(new Uri(NetworkInformation!.BaseUrl!), $"{Version}/auth/refresh");
-            Console.WriteLine($"{finalUri}");
+            Log.Debug("{FinalUri}", finalUri);
             var res = await _httpClient.PostAsync(finalUri, new StringContent(JsonConvert.SerializeObject(new
             {
                 accessToken = AccessToken, refreshToken = RefreshToken
@@ -75,13 +76,12 @@ public partial class Client
             {
                 case HttpStatusCode.OK:
                     AccessToken = parsedRes.Response.AccessToken;
-                    Console.WriteLine("Token refreshed! Happy days.");
+                    Log.Information("Token refreshed! Happy days");
                     var refreshHandlerTask = OnRefresh?.Invoke(parsedRes.Response.AccessToken!, RefreshToken!);
                     if (refreshHandlerTask != null) await refreshHandlerTask;
                     break;
                 default:
-                    await Console.Error.WriteLineAsync(
-                        $"Failed to refresh ({res.StatusCode}) ({parsedRes?.Response?.Message})");
+                    Log.Error("Failed to refresh ({ResStatusCode}) ({ResponseMessage})", res.StatusCode, parsedRes?.Response?.Message);
                     AccessToken = null;
                     RefreshToken = null;
                     break;
@@ -90,22 +90,21 @@ public partial class Client
         }
         catch (Exception ex)
         {
-            Console.Error.WriteLine(ex);
-            await Console.Error.WriteLineAsync($"Failed to log in ({ex.Message})");
+            Log.Error(ex, "Failed to refresh token");
         }
     }
 
     public void LogOut()
     {
-        Console.WriteLine("LogOut");
+        Log.Debug("LogOut");
         AccessToken = null;
         RefreshToken = null;
-        _gateway.Dispose();
+        _gateway?.Dispose();
     }
 
     public void RunLogOut()
     {
-        Console.WriteLine("Logging out!");
+        Log.Debug("Logging out!");
         OnLogOut?.Invoke();
         _refreshTimer?.Dispose();
     }
diff --git a/Lightquark.NET/ClientMethods/Network.cs b/Lightquark.NET/ClientMethods/Network.cs
index 5527ad2ff4b38fee7b2216353228ba68fdc0ab43..e2a43ec3345a6c163d2f53539ac5634db3586513 100644
--- a/Lightquark.NET/ClientMethods/Network.cs
+++ b/Lightquark.NET/ClientMethods/Network.cs
@@ -1,6 +1,7 @@
 using Avalonia.Media.Imaging;
 using Lightquark.NET.Objects;
 using Newtonsoft.Json;
+using Serilog;
 
 namespace Lightquark.NET;
 
@@ -12,7 +13,7 @@ public partial class Client
         try
         {
             var finalUri = new Uri(networkUri, $"{Version}/network");
-            Console.WriteLine($"{finalUri}");
+            Log.Debug("{FinalUri}", finalUri);
             var res = await _httpClient.GetAsync(finalUri);
             if (!res.IsSuccessStatusCode)
                 return (false, $"Could not retrieve network information (code {res.StatusCode})", null);
@@ -22,19 +23,19 @@ public partial class Client
                 return (false,
                     "Malformed network information response. Are you sure this is a Lightquark network?",
                     null);
-            Console.WriteLine(networkInformation.IconUrl);
+            Log.Debug("{IconUrl}",networkInformation.IconUrl);
             var iconRes = await _httpClient.GetAsync(networkInformation.IconUrl);
             networkInformation.Icon = new Bitmap(await iconRes.Content.ReadAsStreamAsync());
             return (true, string.Empty, networkInformation);
         }
         catch (JsonReaderException ex)
         {
-            Console.WriteLine(ex);
+            Log.Information(ex, "Failed to parse network information");
             return (false, "Malformed network information response. Are you sure this is a Lightquark network?", null);
         }
         catch (Exception ex)
         {
-            Console.Error.WriteLine(ex);
+            Log.Error(ex, "Failed to fetch network information");
             return (false, $"Error retrieving network information ({ex.Message})", null);
         }
     }
diff --git a/Lightquark.NET/ClientMethods/User.cs b/Lightquark.NET/ClientMethods/User.cs
index 4c32f1b463b519b0910f1c392f5ec342ffd888cb..8c716ab00fc6d98b8e4f7a37419e9bf179149436 100644
--- a/Lightquark.NET/ClientMethods/User.cs
+++ b/Lightquark.NET/ClientMethods/User.cs
@@ -1,6 +1,7 @@
 using Avalonia.Media.Imaging;
 using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
+using Serilog;
 
 namespace Lightquark.NET;
 
@@ -22,10 +23,8 @@ public partial class Client
         }
         catch (Exception ex)
         {
-            Console.Error.WriteLine(ex);
+            Log.Error(ex, "Failed to get current user from provided object {@User}", user);
         }
         CurrentUser = _mapper.Map(user, CurrentUser);
-        Console.WriteLine(CurrentUser.Status.PrimaryText);
-        Console.WriteLine(CurrentUser.AvatarUriGetter);
     }
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Lightquark.NET.csproj b/Lightquark.NET/Lightquark.NET.csproj
index 2cb7d5bd3070f714c6e970c08cef75f5e60b5c38..3b3b9bf9fa613ad7233670cd4cf13ac2cea5660e 100644
--- a/Lightquark.NET/Lightquark.NET.csproj
+++ b/Lightquark.NET/Lightquark.NET.csproj
@@ -12,6 +12,8 @@
       <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
       <PackageReference Include="Lightquark.Types" Version="2024.9.15.139" />
       <PackageReference Include="Markdig" Version="0.37.0" />
+      <PackageReference Include="Serilog" Version="4.0.1" />
+      <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
       <PackageReference Include="Websocket.Client" Version="5.1.2" />
     </ItemGroup>
 
diff --git a/Lightquark.NET/Util/AsyncManualResetEvent.cs b/Lightquark.NET/Util/AsyncManualResetEvent.cs
new file mode 100644
index 0000000000000000000000000000000000000000..029d0ccf391e45ee81a1e1675763c38b0bbd2495
--- /dev/null
+++ b/Lightquark.NET/Util/AsyncManualResetEvent.cs
@@ -0,0 +1,26 @@
+namespace Lightquark.NET.Util;
+
+public class AsyncManualResetEvent
+{
+    private volatile TaskCompletionSource<bool> _mTcs = new();
+
+    public Task WaitAsync() { return _mTcs.Task; }
+
+    public AsyncManualResetEvent(bool initialState)
+    {
+        if (initialState) Set();
+    }
+    
+    public void Set() { _mTcs.TrySetResult(true); }
+
+    public void Reset()
+    {
+        while (true)
+        {
+            var tcs = _mTcs;
+            if (!tcs.Task.IsCompleted ||
+                Interlocked.CompareExchange(ref _mTcs, new TaskCompletionSource<bool>(), tcs) == tcs)
+                return;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Lightquark.NET/Util/MedianFinder.cs b/Lightquark.NET/Util/MedianFinder.cs
new file mode 100644
index 0000000000000000000000000000000000000000..4acb0fde24557df4655fe7c6269a682ae0ff3406
--- /dev/null
+++ b/Lightquark.NET/Util/MedianFinder.cs
@@ -0,0 +1,18 @@
+namespace Lightquark.NET.Util;
+
+// https://stackoverflow.com/a/8328226
+public class MedianFinder
+{
+    public static double GetMedian(double[] sourceNumbers) {
+        if (sourceNumbers is not { Length: not 0 })
+            throw new Exception("Median of empty array not defined.");
+
+        var sortedPNumbers = (double[])sourceNumbers.Clone();
+        Array.Sort(sortedPNumbers);
+
+        var size = sortedPNumbers.Length;
+        var mid = size / 2;
+        var median = (size % 2 != 0) ? sortedPNumbers[mid] : (sortedPNumbers[mid] + sortedPNumbers[mid - 1]) / 2;
+        return median;
+    }
+}
\ No newline at end of file