diff --git a/src/EllieBot.GrpcApiBase/protos/xp.proto b/src/EllieBot.GrpcApiBase/protos/xp.proto
index faf7b16..07ec339 100644
--- a/src/EllieBot.GrpcApiBase/protos/xp.proto
+++ b/src/EllieBot.GrpcApiBase/protos/xp.proto
@@ -12,6 +12,8 @@ service GrpcXp {
   
   rpc AddReward(AddRewardRequest) returns (AddRewardReply);
   rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
+  
+  rpc GetUserXp(GetUserXpRequest) returns (GetUserXpReply);
 }
 
 message GetXpLbRequest {
@@ -75,4 +77,18 @@ message DeleteRewardRequest {
 
 message DeleteRewardReply {
   bool success = 1;
+}
+
+message GetUserXpRequest {
+  uint64 guildId = 1;
+  uint64 userId = 2;
+}
+
+message GetUserXpReply {
+  int64 xp = 1;
+  int64 requiredXp = 2;
+  int64 level = 3;
+  string club = 4;
+  string clubIcon = 5;
+  int32 rank = 6;
 }
\ No newline at end of file
diff --git a/src/EllieBot.GrpcApiBase/protos/xpshop.proto b/src/EllieBot.GrpcApiBase/protos/xpshop.proto
new file mode 100644
index 0000000..298e700
--- /dev/null
+++ b/src/EllieBot.GrpcApiBase/protos/xpshop.proto
@@ -0,0 +1,71 @@
+syntax = "proto3";
+
+option csharp_namespace = "EllieBot.GrpcApi";
+
+package greet;
+
+service GrpcXpShop {
+  rpc AddXpShopItem (AddXpShopItemRequest) returns (AddXpShopItemReply);
+  rpc GetShopItems (GetShopItemsRequest) returns (GetShopItemsReply);
+  rpc UseShopItem (UseShopItemRequest) returns (UseShopItemReply);
+  rpc BuyShopItem (BuyShopItemRequest) returns (BuyShopItemReply);
+}
+
+message UseShopItemRequest {
+  uint64 userId = 1;
+  string uniqueName = 2;
+  GrpcXpShopItemType itemType = 3;
+}
+
+message UseShopItemReply {
+  bool success = 1;
+}
+
+message BuyShopItemRequest {
+  uint64 userId = 1;
+  string uniqueName = 2;
+  GrpcXpShopItemType itemType = 3;
+}
+
+message BuyShopItemReply {
+  bool success = 1;
+  optional BuyShopItemError Error = 2;
+}
+
+enum BuyShopItemError {
+  NotEnough = 0;
+  AlreadyOwned = 1;
+  Unknown = 2;
+}
+
+message AddXpShopItemRequest {
+  XpShopItem item = 1;
+  string uniqueName = 2;
+  GrpcXpShopItemType itemType = 3;
+}
+
+message AddXpShopItemReply {
+  bool success = 1;
+}
+
+message GetShopItemsRequest {
+
+}
+
+message GetShopItemsReply {
+  repeated XpShopItem bgs = 1;
+  repeated XpShopItem frames = 2;
+}
+
+message XpShopItem {
+  string Name = 1;
+  string Description = 2;
+  int64 Price = 3;
+  string FullUrl = 4;
+  string PreviewUrl = 5;
+}
+
+enum GrpcXpShopItemType {
+  Bg = 0;
+  Frame = 1;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs
index d2a9069..927ca93 100644
--- a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs
+++ b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs
@@ -40,7 +40,9 @@ public partial class Administration
             var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
             var progress = GetProgressTracker(progressMsg);
 
-            var result = await _service.PruneWhere(ctx.Channel,
+            var result = await _service.PruneWhere(
+                ctx.User.Id,
+                ctx.Channel,
                 100,
                 x => x.Author.Id == ctx.Client.CurrentUser.Id,
                 progress);
@@ -66,13 +68,17 @@ public partial class Administration
 
             PruneResult result;
             if (opts.Safe)
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    (ITextChannel)ctx.Channel,
                     100,
                     x => x.Author.Id == user.Id && !x.IsPinned,
                     progress,
                     opts.After);
             else
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    (ITextChannel)ctx.Channel,
                     100,
                     x => x.Author.Id == user.Id,
                     progress,
@@ -107,13 +113,17 @@ public partial class Administration
 
             PruneResult result;
             if (opts.Safe)
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    ctx.Channel,
                     count,
                     x => !x.IsPinned && x.Id != progressMsg.Id,
                     progress,
                     opts.After);
             else
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    ctx.Channel,
                     count,
                     x => x.Id != progressMsg.Id,
                     progress,
@@ -133,13 +143,14 @@ public partial class Administration
                     await progressMsg.ModifyAsync(props =>
                     {
                         props.Embed = CreateEmbed()
-                                      .WithPendingColor()
-                                      .WithDescription(GetText(strs.prune_progress(deleted, total)))
-                                      .Build();
+                            .WithPendingColor()
+                            .WithDescription(GetText(strs.prune_progress(deleted, total)))
+                            .Build();
                     });
                 }
                 catch
                 {
+                    // ignored
                 }
             });
 
@@ -182,7 +193,9 @@ public partial class Administration
             PruneResult result;
             if (opts.Safe)
             {
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    ctx.Channel,
                     count,
                     m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
                     progress,
@@ -191,7 +204,9 @@ public partial class Administration
             }
             else
             {
-                result = await _service.PruneWhere((ITextChannel)ctx.Channel,
+                result = await _service.PruneWhere(
+                    ctx.User.Id,
+                    ctx.Channel,
                     count,
                     m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
                     progress,
@@ -233,7 +248,7 @@ public partial class Administration
                     msg.DeleteAfter(5);
                     break;
                 case PruneResult.FeatureLimit:
-                    var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
+                    var msg2 = await Response().Pending(strs.prune_patron).SendAsync();
                     msg2.DeleteAfter(10);
                     break;
                 default:
diff --git a/src/EllieBot/Modules/Administration/Prune/PruneService.cs b/src/EllieBot/Modules/Administration/Prune/PruneService.cs
index f90ec43..88e7fd2 100644
--- a/src/EllieBot/Modules/Administration/Prune/PruneService.cs
+++ b/src/EllieBot/Modules/Administration/Prune/PruneService.cs
@@ -1,23 +1,13 @@
 #nullable disable
-using EllieBot.Modules.Patronage;
-
 namespace EllieBot.Modules.Administration.Services;
 
-public class PruneService : IEService
+public class PruneService(ILogCommandService logService) : IEService
 {
-    //channelids where prunes are currently occuring
     private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
     private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
-    private readonly ILogCommandService _logService;
-    private readonly IPatronageService _ps;
-
-    public PruneService(ILogCommandService logService, IPatronageService ps)
-    {
-        _logService = logService;
-        _ps = ps;
-    }
 
     public async Task<PruneResult> PruneWhere(
+        ulong runningUserId,
         IMessageChannel channel,
         int amount,
         Func<IMessage, bool> predicate,
@@ -37,11 +27,6 @@ public class PruneService : IEService
 
         try
         {
-            if (channel is ITextChannel tc && !await _ps.LimitHitAsync(LimitedFeatureName.Prune, tc.Guild.OwnerId))
-            {
-                return PruneResult.FeatureLimit;
-            }
-
             var now = DateTime.UtcNow;
             IMessage[] msgs;
             IMessage lastMessage = null;
@@ -67,7 +52,7 @@ public class PruneService : IEService
                 var singleDeletable = new List<IMessage>();
                 foreach (var x in msgs)
                 {
-                    _logService.AddDeleteIgnore(x.Id);
+                    logService.AddDeleteIgnore(x.Id);
 
                     if (now - x.CreatedAt < _twoWeeks)
                         bulkDeletable.Add(x);
diff --git a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs
index 0d947ac..a817b01 100644
--- a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs
+++ b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs
@@ -57,8 +57,7 @@ public partial class Administration
                 _ => ctx.OkAsync(),
                 async fl =>
                 {
-                    _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
-                    await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
+                    await msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
                 });
         }
 
diff --git a/src/EllieBot/Modules/Xp/BuyResult.cs b/src/EllieBot/Modules/Xp/BuyResult.cs
index 3c4c464..e4a2d32 100644
--- a/src/EllieBot/Modules/Xp/BuyResult.cs
+++ b/src/EllieBot/Modules/Xp/BuyResult.cs
@@ -7,5 +7,4 @@ public enum BuyResult
     AlreadyOwned,
     InsufficientFunds,
     UnknownItem,
-    InsufficientPatronTier,
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs
index 3934005..78d10cd 100644
--- a/src/EllieBot/Modules/Xp/Xp.cs
+++ b/src/EllieBot/Modules/Xp/Xp.cs
@@ -327,12 +327,6 @@ public partial class Xp : EllieModule<XpService>
                   if (!string.IsNullOrWhiteSpace(item.Desc))
                       eb.AddField(GetText(strs.desc), item.Desc);
 
-                  var tier = _service.GetXpShopTierRequirement(type);
-                  if (tier != PatronTier.None)
-                  {
-                      eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString())));
-                  }
-
                   return eb;
               })
               .Interaction(async current =>
@@ -407,7 +401,6 @@ public partial class Xp : EllieModule<XpService>
                 BuyResult.AlreadyOwned =>
                     await Response().Error(strs.xpshop_already_owned).Interaction(GetUseInteraction()).SendAsync(),
                 BuyResult.UnknownItem => await Response().Error(strs.xpshop_item_not_found).SendAsync(),
-                BuyResult.InsufficientPatronTier => await Response().Error(strs.patron_insuff_tier).SendAsync(),
                 _ => throw new ArgumentOutOfRangeException()
             };
             return;
diff --git a/src/EllieBot/Modules/Xp/XpConfig.cs b/src/EllieBot/Modules/Xp/XpConfig.cs
index cb0e120..1931cb6 100644
--- a/src/EllieBot/Modules/Xp/XpConfig.cs
+++ b/src/EllieBot/Modules/Xp/XpConfig.cs
@@ -10,7 +10,7 @@ namespace EllieBot.Modules.Xp;
 public sealed partial class XpConfig : ICloneable<XpConfig>
 {
     [Comment("""DO NOT CHANGE""")]
-    public int Version { get; set; } = 10;
+    public int Version { get; set; } = 11;
 
     [Comment("""How much XP will the users receive per message""")]
     public int TextXpPerMessage { get; set; } = 3;
@@ -36,18 +36,6 @@ public sealed partial class XpConfig : ICloneable<XpConfig>
                  """)]
         public bool IsEnabled { get; set; } = false;
 
-        [Comment("""
-                 Which patron tier do users need in order to use the .xpshop bgs command
-                 Leave at 'None' if patron system is disabled or you don't want any restrictions
-                 """)]
-        public PatronTier BgsTierRequirement { get; set; } = PatronTier.None;
-
-        [Comment("""
-                 Which patron tier do users need in order to use the .xpshop frames command
-                 Leave at 'None' if patron system is disabled or you don't want any restrictions
-                 """)]
-        public PatronTier FramesTierRequirement { get; set; } = PatronTier.None;
-
         [Comment("""
                  Frames available for sale. Keys are unique IDs.
                  Do not change keys as they are not publicly visible. Only change properties (name, price, id)
diff --git a/src/EllieBot/Modules/Xp/XpConfigService.cs b/src/EllieBot/Modules/Xp/XpConfigService.cs
index 1f1eb3d..c4459ba 100644
--- a/src/EllieBot/Modules/Xp/XpConfigService.cs
+++ b/src/EllieBot/Modules/Xp/XpConfigService.cs
@@ -1,5 +1,6 @@
 #nullable disable
 using EllieBot.Common.Configs;
+using EllieBot.Db.Models;
 
 namespace EllieBot.Modules.Xp.Services;
 
@@ -48,12 +49,27 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
 
     private void Migrate()
     {
-        if (data.Version < 10)
+        if (data.Version < 11)
         {
-            ModifyConfig(c =>
-            {
-                c.Version = 10;
-            });
+            ModifyConfig(c => { c.Version = 11; });
         }
     }
+
+    public async Task<bool> AddItemAsync(string uniqueName, XpShopItemType itemType, XpConfig.ShopItemInfo shopItemInfo)
+    {
+        await Task.Yield();
+
+        var success = false;
+        ModifyConfig(c =>
+        {
+            var items = itemType == XpShopItemType.Background
+                ? c.Shop.Bgs
+                : c.Shop.Frames;
+
+            if (items is not null)
+                success = items.TryAdd(uniqueName, shopItemInfo);
+        });
+
+        return success;
+    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs
index bb6c165..58def50 100644
--- a/src/EllieBot/Modules/Xp/XpService.cs
+++ b/src/EllieBot/Modules/Xp/XpService.cs
@@ -989,18 +989,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         if (!conf.Shop.IsEnabled)
             return BuyResult.XpShopDisabled;
 
-        var req = type == XpShopItemType.Background
-            ? conf.Shop.BgsTierRequirement
-            : conf.Shop.FramesTierRequirement;
-
-        if (req != PatronTier.None && !_creds.IsOwner(userId))
-        {
-            var patron = await _ps.GetPatronAsync(userId);
-
-            if (patron is null || (int)patron.Value.Tier < (int)req)
-                return BuyResult.InsufficientPatronTier;
-        }
-
         await using var ctx = _db.GetDbContext();
         try
         {
@@ -1127,13 +1115,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         return false;
     }
 
-    public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type)
-        => type switch
-        {
-            Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement,
-            _ => PatronTier.None,
-        };
-
     public bool IsShopEnabled()
         => _xpConfig.Data.Shop.IsEnabled;
 
diff --git a/src/EllieBot/Services/GrpcApi/XpShopSvc.cs b/src/EllieBot/Services/GrpcApi/XpShopSvc.cs
new file mode 100644
index 0000000..9f25eb8
--- /dev/null
+++ b/src/EllieBot/Services/GrpcApi/XpShopSvc.cs
@@ -0,0 +1,93 @@
+using Grpc.Core;
+using EllieBot.Db.Models;
+using EllieBot.Modules.Xp;
+using EllieBot.Modules.Xp.Services;
+
+namespace EllieBot.GrpcApi;
+
+public class XpShopSvc(XpService xp, XpConfigService xpConfig) : GrpcXpShop.GrpcXpShopBase, IGrpcSvc, IEService
+{
+    public ServerServiceDefinition Bind()
+        => GrpcXpShop.BindService(this);
+
+    public override async Task<BuyShopItemReply> BuyShopItem(BuyShopItemRequest request, ServerCallContext context)
+    {
+        var result = await xp.BuyShopItemAsync(request.UserId, (XpShopItemType)request.ItemType, request.UniqueName);
+
+        var res = new BuyShopItemReply();
+
+        if (result == BuyResult.Success)
+        {
+            res.Success = true;
+            return res;
+        }
+
+        res.Error = result switch
+        {
+            BuyResult.AlreadyOwned => BuyShopItemError.AlreadyOwned,
+            BuyResult.InsufficientFunds => BuyShopItemError.NotEnough,
+            _ => BuyShopItemError.Unknown
+        };
+
+        return res;
+    }
+
+    public override async Task<UseShopItemReply> UseShopItem(UseShopItemRequest request, ServerCallContext context)
+    {
+        var result = await xp.UseShopItemAsync(request.UserId, (XpShopItemType)request.ItemType, request.UniqueName);
+
+        var res = new UseShopItemReply
+        {
+            Success = result
+        };
+
+        return res;
+    }
+
+    public override async Task<GetShopItemsReply> GetShopItems(GetShopItemsRequest request, ServerCallContext context)
+    {
+        var bgsTask = Task.Run(async () => await xp.GetShopBgs());
+        var frsTask = Task.Run(async () => await xp.GetShopFrames());
+
+        var bgs = await bgsTask.Fmap(x => x?.Map(y => MapItemToGrpcItem(y.Value, y.Key)) ?? []);
+        var frs = await frsTask.Fmap(z => z?.Map(y => MapItemToGrpcItem(y.Value, y.Key)) ?? []);
+
+        var res = new GetShopItemsReply();
+
+        res.Bgs.AddRange(bgs);
+        res.Frames.AddRange(frs);
+
+        return res;
+
+        static XpShopItem MapItemToGrpcItem(XpConfig.ShopItemInfo item, string uniqueName)
+        {
+            return new XpShopItem()
+            {
+                Name = item.Name,
+                Price = item.Price,
+                Description = item.Desc,
+                FullUrl = item.Url,
+                PreviewUrl = item.Preview,
+            };
+        }
+    }
+
+    public override async Task<AddXpShopItemReply> AddXpShopItem(AddXpShopItemRequest request,
+        ServerCallContext context)
+    {
+        var result = await xpConfig.AddItemAsync(request.UniqueName, (XpShopItemType)request.ItemType,
+            new XpConfig.ShopItemInfo()
+            {
+                Name = request.Item.Name,
+                Price = 3000,
+                Desc = request.Item.Description,
+                Url = request.Item.FullUrl,
+                Preview = request.Item.PreviewUrl,
+            });
+
+        return new AddXpShopItemReply()
+        {
+            Success = result,
+        };
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Services/GrpcApi/XpSvc.cs b/src/EllieBot/Services/GrpcApi/XpSvc.cs
index 8aced25..01c9b5c 100644
--- a/src/EllieBot/Services/GrpcApi/XpSvc.cs
+++ b/src/EllieBot/Services/GrpcApi/XpSvc.cs
@@ -193,4 +193,51 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
 
         return reply;
     }
+    
+    /// <summary>
+    /// Gets XP information for a specific user in a guild
+    /// </summary>
+    public override async Task<GetUserXpReply> GetUserXp(
+        GetUserXpRequest request,
+        ServerCallContext context)
+    {
+        var guild = _client.GetGuild(request.GuildId);
+         
+        if (guild is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
+             
+        var user = guild.GetUser(request.UserId);
+         
+        if (user is null)
+            throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
+             
+        var reply = new GetUserXpReply();
+         
+        // Get user stats from the XP service
+        var stats = await _xp.GetUserStatsAsync(user);
+        var levelStats = stats.Guild;
+         
+        // Get user's rank in guild
+        var guildRank = stats.GuildRanking;
+         
+        // Fill the response with user XP data
+        reply.Xp = levelStats.LevelXp;
+        reply.RequiredXp = levelStats.RequiredXp;
+        reply.Level = levelStats.Level;
+        reply.Rank = guildRank;
+         
+        // Add club information if available
+        if (stats.User.Club is not null)
+        {
+            reply.Club = stats.User.Club.ToString();
+            reply.ClubIcon = stats.User.Club.ImageUrl ?? string.Empty;
+        }
+        else
+        {
+            reply.Club = string.Empty;
+            reply.ClubIcon = string.Empty;
+        }
+         
+        return reply;
+    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json
index 8346ad8..6ad7a04 100644
--- a/src/EllieBot/strings/responses/responses.en-US.json
+++ b/src/EllieBot/strings/responses/responses.en-US.json
@@ -1045,10 +1045,8 @@
   "bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
   "cmd_group_commands": "'{0}' command group",
   "limit_reached": "Feature limit of {0} reached.",
-  "feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.",
-  "feature_limit_reached_owner": "Feature limit reached. Server owner may upgrade patron level to increase the limit.",
-  "feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.",
-  "xp_shop_buy_required_tier": "Buying items from this shop requires Patron Tier {0} or higher.",
+  "feature_limit": "The limit of {0} for the {1} feature has been reached. Server owner may be able to increase the limit by upgrading the Patron Tier.",
+  "prune_patron": "Deleting messages 2 weeks old or older requires [Patron Tier X](https://patreon.com/join/elliebot) or higher.",
   "available_commands": "Available Commands",
   "tier": "Tier",
   "pledge": "Pledge",