diff --git a/src/EllieBot.GrpcApiBase/protos/xp.proto b/src/EllieBot.GrpcApiBase/protos/xp.proto
new file mode 100644
index 0000000..84c4daa
--- /dev/null
+++ b/src/EllieBot.GrpcApiBase/protos/xp.proto
@@ -0,0 +1,108 @@
+syntax = "proto3";
+
+option csharp_namespace = "EllieBot.GrpcApi";
+
+package xp;
+
+service GrpcXp {
+  rpc GetXpLb(GetXpLbRequest) returns (GetXpLbReply);
+  rpc ResetUserXp(ResetUserXpRequest) returns (ResetUserXpReply);
+  
+  rpc GetXpSettings(GetXpSettingsRequest) returns (GetXpSettingsReply);
+  
+  rpc AddExclusion(AddExclusionRequest) returns (AddExclusionReply);
+  rpc DeleteExclusion(DeleteExclusionRequest) returns (DeleteExclusionReply);
+  
+  rpc AddReward(AddRewardRequest) returns (AddRewardReply);
+  rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
+}
+
+message GetXpLbRequest {
+  uint64 guildId = 1;
+  int32 page = 2;
+}
+
+message GetXpLbReply {
+  repeated XpLbUserReply users = 1;
+  int32 total = 2;
+}
+
+message XpLbUserReply {
+  uint64 userId = 1;
+  string username = 2;
+  int64 xp = 3;
+  int64 level = 4;
+  string avatar = 5;
+}
+
+message ResetUserXpRequest {
+  uint64 guildId = 1;
+  uint64 userId = 2;
+}
+
+message ResetUserXpReply {
+  bool success = 1;
+}
+
+message GetXpSettingsReply {
+  repeated ExclItemReply exclusions = 1;
+  repeated RewItemReply rewards = 2;
+  bool serverExcluded = 3;
+}
+
+message GetXpSettingsRequest {
+  uint64 guildId = 1;
+}
+
+message ExclItemReply {
+  string type = 1;
+  uint64 id = 2;
+  string name = 3;
+}
+
+message RewItemReply {
+  int32 level = 1;
+  string type = 2;
+  string value = 3;
+}
+
+message AddExclusionRequest {
+  uint64 guildId = 1;
+  string type = 2;
+  uint64 id = 3;
+}
+
+message AddExclusionReply {
+  bool success = 1;
+}
+
+message DeleteExclusionRequest {
+  uint64 guildId = 1;
+  string type = 2;
+  uint64 id = 3;
+}
+
+message DeleteExclusionReply {
+  bool success = 1;
+}
+
+message AddRewardRequest {
+  uint64 guildId = 1;
+  int32 level = 2;
+  string type = 3;
+  string value = 4;
+}
+
+message AddRewardReply {
+  bool success = 1;
+}
+
+message DeleteRewardRequest {
+  uint64 guildId = 1;
+  int32 level = 2;
+  string type = 3;
+}
+
+message DeleteRewardReply {
+  bool success = 1;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Services/GrpcApi/XpSvc.cs b/src/EllieBot/Services/GrpcApi/XpSvc.cs
new file mode 100644
index 0000000..287c71e
--- /dev/null
+++ b/src/EllieBot/Services/GrpcApi/XpSvc.cs
@@ -0,0 +1,245 @@
+using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
+using EllieBot.Db.Models;
+using EllieBot.Modules.Gambling.Bank;
+using EllieBot.Modules.EllieExpressions;
+using EllieBot.Modules.Utility;
+using EllieBot.Modules.Xp.Services;
+
+namespace EllieBot.GrpcApi;
+
+public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
+{
+    private readonly XpService _xp;
+    private readonly DiscordSocketClient _client;
+    private readonly IUserService _duSvc;
+
+    public XpSvc(XpService xp, DiscordSocketClient client, IUserService duSvc)
+    {
+        _xp = xp;
+        _client = client;
+        _duSvc = duSvc;
+    }
+
+    public ServerServiceDefinition Bind()
+        => GrpcXp.BindService(this);
+
+    public override async Task<GetXpSettingsReply> GetXpSettings(
+        GetXpSettingsRequest request,
+        ServerCallContext context)
+    {
+        var guild = _client.GetGuild(request.GuildId);
+
+        if (guild is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
+
+        var excludedChannels = _xp.GetExcludedChannels(request.GuildId);
+        var excludedRoles = _xp.GetExcludedRoles(request.GuildId);
+        var isServerExcluded = _xp.IsServerExcluded(request.GuildId);
+
+        var reply = new GetXpSettingsReply();
+
+        reply.Exclusions.AddRange(excludedChannels
+                                  .Select(x => new ExclItemReply()
+                                  {
+                                      Id = x,
+                                      Type = "Channel",
+                                      Name = guild.GetChannel(x)?.Name ?? "????"
+                                  })
+                                  .Concat(excludedRoles
+                                      .Select(x => new ExclItemReply()
+                                      {
+                                          Id = x,
+                                          Type = "Role",
+                                          Name = guild.GetRole(x)?.Name ?? "????"
+                                      })));
+
+        reply.ServerExcluded = isServerExcluded;
+
+        return reply;
+    }
+
+    public override async Task<AddExclusionReply> AddExclusion(AddExclusionRequest request, ServerCallContext context)
+    {
+        await Task.Yield();
+
+        var success = false;
+        var guild = _client.GetGuild(request.GuildId);
+
+        if (guild is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
+
+        if (request.Type == "Role")
+        {
+            if (guild.GetRole(request.Id) is null)
+                return new()
+                {
+                    Success = false
+                };
+
+            success = _xp.ToggleExcludeRole(request.GuildId, request.Id);
+        }
+        else if (request.Type == "Channel")
+        {
+            if (guild.GetTextChannel(request.Id) is null)
+                return new()
+                {
+                    Success = false
+                };
+
+            success = _xp.ToggleExcludeChannel(request.GuildId, request.Id);
+        }
+
+        return new()
+        {
+            Success = success
+        };
+    }
+
+    public override Task<DeleteExclusionReply> DeleteExclusion(
+        DeleteExclusionRequest request,
+        ServerCallContext context)
+    {
+        var success = false;
+        if (request.Type == "Role")
+            success = _xp.ToggleExcludeRole(request.GuildId, request.Id);
+        else
+            success = _xp.ToggleExcludeChannel(request.GuildId, request.Id);
+
+        return Task.FromResult(new DeleteExclusionReply
+        {
+            Success = success
+        });
+    }
+
+    public override async Task<AddRewardReply> AddReward(AddRewardRequest request, ServerCallContext context)
+    {
+        await Task.Yield();
+
+        var success = false;
+        var guild = _client.GetGuild(request.GuildId);
+
+        if (guild is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
+
+        if (request.Type == "AddRole" || request.Type == "RemoveRole")
+        {
+            if (!ulong.TryParse(request.Value, out var rid))
+                throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid role id"));
+
+            var role = guild.GetRole(rid);
+            if (role is null)
+                return new()
+                {
+                    Success = false
+                };
+
+            _xp.SetRoleReward(request.GuildId, request.Level, rid, request.Type == "RemoveRole");
+            success = true;
+        }
+        else if (request.Type == "Currency")
+        {
+            if (!int.TryParse(request.Value, out var amount))
+                throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount"));
+
+            _xp.SetCurrencyReward(request.GuildId, request.Level, amount);
+            success = true;
+        }
+
+        return new()
+        {
+            Success = success
+        };
+    }
+
+    public override Task<DeleteRewardReply> DeleteReward(DeleteRewardRequest request, ServerCallContext context)
+    {
+        var success = false;
+
+        if (request.Type == "AddRole" || request.Type == "RemoveRole")
+        {
+            _xp.ResetRoleReward(request.GuildId, request.Level);
+            success = true;
+        }
+        else if (request.Type == "Currency")
+        {
+            _xp.SetCurrencyReward(request.GuildId, request.Level, 0);
+            success = true;
+        }
+
+        return Task.FromResult(new DeleteRewardReply
+        {
+            Success = success
+        });
+    }
+
+    public override async Task<ResetUserXpReply> ResetUserXp(ResetUserXpRequest request, ServerCallContext context)
+    {
+        await _xp.XpReset(request.GuildId, request.UserId);
+
+        return new ResetUserXpReply
+        {
+            Success = true
+        };
+    }
+
+    public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context)
+    {
+        if (request.Page < 0)
+            throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 0"));
+
+        var guild = _client.GetGuild(request.GuildId);
+
+        if (guild is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
+
+        var data = await _xp.GetGuildUserXps(request.GuildId, request.Page);
+        var total = await _xp.GetTotalGuildUsers(request.GuildId);
+
+        var reply = new GetXpLbReply
+        {
+            Total = total
+        };
+
+        reply.Users.AddRange(await data
+                                   .Select(async x =>
+                                   {
+                                       var user = guild.GetUser(x.UserId);
+
+                                       if (user is null)
+                                       {
+                                           var du = await _duSvc.GetUserAsync(x.UserId);
+                                           if (du is null)
+                                               return new XpLbUserReply
+                                               {
+                                                   UserId = x.UserId,
+                                                   Avatar = string.Empty,
+                                                   Username = string.Empty,
+                                                   Xp = x.Xp,
+                                                   Level = new LevelStats(x.Xp).Level
+                                               };
+
+                                           return new XpLbUserReply()
+                                           {
+                                               UserId = x.UserId,
+                                               Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
+                                               Username = du.ToString() ?? string.Empty,
+                                               Xp = x.Xp,
+                                               Level = new LevelStats(x.Xp).Level
+                                           };
+                                       }
+
+                                       return new XpLbUserReply
+                                       {
+                                           UserId = x.UserId,
+                                           Avatar = user?.GetAvatarUrl() ?? string.Empty,
+                                           Username = user?.ToString() ?? string.Empty,
+                                           Xp = x.Xp,
+                                           Level = new LevelStats(x.Xp).Level
+                                       };
+                                   })
+                                   .WhenAll());
+
+        return reply;
+    }
+}
\ No newline at end of file