diff --git a/src/EllieBot/Db/Models/xp/ExcludedItem.cs b/src/EllieBot/Db/Models/xp/ExcludedItem.cs
deleted file mode 100644
index a0b8b00..0000000
--- a/src/EllieBot/Db/Models/xp/ExcludedItem.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace EllieBot.Db.Models;
-
-public class ExcludedItem : DbEntity
-{
-    public int? XpSettingsId { get; set; }
-    public ulong ItemId { get; set; }
-    public ExcludedItemType ItemType { get; set; }
-
-    public override int GetHashCode()
-        => ItemId.GetHashCode() ^ ItemType.GetHashCode();
-
-    public override bool Equals(object? obj)
-        => obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType;
-}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/xp/ExcludedItemType.cs b/src/EllieBot/Db/Models/xp/ExcludedItemType.cs
index e6b1e6a..f001061 100644
--- a/src/EllieBot/Db/Models/xp/ExcludedItemType.cs
+++ b/src/EllieBot/Db/Models/xp/ExcludedItemType.cs
@@ -1,3 +1,7 @@
 namespace EllieBot.Db.Models;
 
-public enum ExcludedItemType { Channel, Role }
\ No newline at end of file
+public enum XpExcludedItemType
+{
+    User,
+    Role
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/xp/XpExcludedItem.cs b/src/EllieBot/Db/Models/xp/XpExcludedItem.cs
new file mode 100644
index 0000000..d4c93cd
--- /dev/null
+++ b/src/EllieBot/Db/Models/xp/XpExcludedItem.cs
@@ -0,0 +1,31 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace EllieBot.Db.Models;
+
+public class XpExcludedItem
+{
+    [Key]
+    public int Id { get; set; }
+
+    public ulong GuildId { get; set; }
+
+    public XpExcludedItemType ItemType { get; set; }
+    public ulong ItemId { get; set; }
+}
+
+public sealed class XpExclusionEntityConfig : IEntityTypeConfiguration<XpExcludedItem>
+{
+    public void Configure(EntityTypeBuilder<XpExcludedItem> builder)
+    {
+        builder.HasIndex(x => x.GuildId);
+
+        builder.HasAlternateKey(x => new
+        {
+            x.GuildId,
+            x.ItemType,
+            x.ItemId
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/20250318221943_xpexclusion.sql b/src/EllieBot/Migrations/PostgreSql/20250318221943_xpexclusion.sql
new file mode 100644
index 0000000..a0714f8
--- /dev/null
+++ b/src/EllieBot/Migrations/PostgreSql/20250318221943_xpexclusion.sql
@@ -0,0 +1,17 @@
+START TRANSACTION;
+CREATE TABLE xpexcludeditem (
+    id integer GENERATED BY DEFAULT AS IDENTITY,
+    guildid numeric(20,0) NOT NULL,
+    itemtype integer NOT NULL,
+    itemid numeric(20,0) NOT NULL,
+    CONSTRAINT pk_xpexcludeditem PRIMARY KEY (id),
+    CONSTRAINT ak_xpexcludeditem_guildid_itemtype_itemid UNIQUE (guildid, itemtype, itemid)
+);
+
+CREATE INDEX ix_xpexcludeditem_guildid ON xpexcludeditem (guildid);
+
+INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
+VALUES ('20250318221943_xpexclusion', '9.0.1');
+
+COMMIT;
+
diff --git a/src/EllieBot/Migrations/PostgreSql/20250317063309_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs
similarity index 99%
rename from src/EllieBot/Migrations/PostgreSql/20250317063309_init.Designer.cs
rename to src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs
index b95382f..db697cd 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250317063309_init.Designer.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs
@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace EllieBot.Migrations.PostgreSql
 {
     [DbContext(typeof(PostgreSqlContext))]
-    [Migration("20250317063309_init")]
+    [Migration("20250318222207_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -3263,6 +3263,39 @@ namespace EllieBot.Migrations.PostgreSql
                     b.ToTable("xpcurrencyreward", (string)null);
                 });
 
+            modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<decimal>("GuildId")
+                        .HasColumnType("numeric(20,0)")
+                        .HasColumnName("guildid");
+
+                    b.Property<decimal>("ItemId")
+                        .HasColumnType("numeric(20,0)")
+                        .HasColumnName("itemid");
+
+                    b.Property<int>("ItemType")
+                        .HasColumnType("integer")
+                        .HasColumnName("itemtype");
+
+                    b.HasKey("Id")
+                        .HasName("pk_xpexcludeditem");
+
+                    b.HasAlternateKey("GuildId", "ItemType", "ItemId")
+                        .HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
+
+                    b.HasIndex("GuildId")
+                        .HasDatabaseName("ix_xpexcludeditem_guildid");
+
+                    b.ToTable("xpexcludeditem", (string)null);
+                });
+
             modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
                 {
                     b.Property<int>("Id")
diff --git a/src/EllieBot/Migrations/PostgreSql/20250317063309_init.cs b/src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs
similarity index 99%
rename from src/EllieBot/Migrations/PostgreSql/20250317063309_init.cs
rename to src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs
index d1ca439..5278da3 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250317063309_init.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs
@@ -1179,6 +1179,22 @@ namespace EllieBot.Migrations.PostgreSql
                     table.PrimaryKey("pk_warnings", x => x.id);
                 });
 
+            migrationBuilder.CreateTable(
+                name: "xpexcludeditem",
+                columns: table => new
+                {
+                    id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
+                    itemtype = table.Column<int>(type: "integer", nullable: false),
+                    itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("pk_xpexcludeditem", x => x.id);
+                    table.UniqueConstraint("ak_xpexcludeditem_guildid_itemtype_itemid", x => new { x.guildid, x.itemtype, x.itemid });
+                });
+
             migrationBuilder.CreateTable(
                 name: "xpsettings",
                 columns: table => new
@@ -2280,6 +2296,11 @@ namespace EllieBot.Migrations.PostgreSql
                 table: "xpcurrencyreward",
                 column: "xpsettingsid");
 
+            migrationBuilder.CreateIndex(
+                name: "ix_xpexcludeditem_guildid",
+                table: "xpexcludeditem",
+                column: "guildid");
+
             migrationBuilder.CreateIndex(
                 name: "ix_xprolereward_xpsettingsid_level",
                 table: "xprolereward",
@@ -2574,6 +2595,9 @@ namespace EllieBot.Migrations.PostgreSql
             migrationBuilder.DropTable(
                 name: "xpcurrencyreward");
 
+            migrationBuilder.DropTable(
+                name: "xpexcludeditem");
+
             migrationBuilder.DropTable(
                 name: "xprolereward");
 
diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
index 36d701a..e9a47ca 100644
--- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
@@ -3260,6 +3260,39 @@ namespace EllieBot.Migrations.PostgreSql
                     b.ToTable("xpcurrencyreward", (string)null);
                 });
 
+            modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<decimal>("GuildId")
+                        .HasColumnType("numeric(20,0)")
+                        .HasColumnName("guildid");
+
+                    b.Property<decimal>("ItemId")
+                        .HasColumnType("numeric(20,0)")
+                        .HasColumnName("itemid");
+
+                    b.Property<int>("ItemType")
+                        .HasColumnType("integer")
+                        .HasColumnName("itemtype");
+
+                    b.HasKey("Id")
+                        .HasName("pk_xpexcludeditem");
+
+                    b.HasAlternateKey("GuildId", "ItemType", "ItemId")
+                        .HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
+
+                    b.HasIndex("GuildId")
+                        .HasDatabaseName("ix_xpexcludeditem_guildid");
+
+                    b.ToTable("xpexcludeditem", (string)null);
+                });
+
             modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
                 {
                     b.Property<int>("Id")
diff --git a/src/EllieBot/Migrations/Sqlite/20250318221922_xpexclusion.sql b/src/EllieBot/Migrations/Sqlite/20250318221922_xpexclusion.sql
new file mode 100644
index 0000000..19fee90
--- /dev/null
+++ b/src/EllieBot/Migrations/Sqlite/20250318221922_xpexclusion.sql
@@ -0,0 +1,16 @@
+BEGIN TRANSACTION;
+CREATE TABLE "XpExcludedItem" (
+    "Id" INTEGER NOT NULL CONSTRAINT "PK_XpExcludedItem" PRIMARY KEY AUTOINCREMENT,
+    "GuildId" INTEGER NOT NULL,
+    "ItemType" INTEGER NOT NULL,
+    "ItemId" INTEGER NOT NULL,
+    CONSTRAINT "AK_XpExcludedItem_GuildId_ItemType_ItemId" UNIQUE ("GuildId", "ItemType", "ItemId")
+);
+
+CREATE INDEX "IX_XpExcludedItem_GuildId" ON "XpExcludedItem" ("GuildId");
+
+INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
+VALUES ('20250318221922_xpexclusion', '9.0.1');
+
+COMMIT;
+
diff --git a/src/EllieBot/Migrations/Sqlite/20250317063300_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs
similarity index 99%
rename from src/EllieBot/Migrations/Sqlite/20250317063300_init.Designer.cs
rename to src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs
index 93429ce..969d466 100644
--- a/src/EllieBot/Migrations/Sqlite/20250317063300_init.Designer.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs
@@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace EllieBot.Migrations.Sqlite
 {
     [DbContext(typeof(SqliteContext))]
-    [Migration("20250317063300_init")]
+    [Migration("20250318222152_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -2428,6 +2428,30 @@ namespace EllieBot.Migrations.Sqlite
                     b.ToTable("XpCurrencyReward");
                 });
 
+            modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<ulong>("GuildId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<ulong>("ItemId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ItemType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasAlternateKey("GuildId", "ItemType", "ItemId");
+
+                    b.HasIndex("GuildId");
+
+                    b.ToTable("XpExcludedItem");
+                });
+
             modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
                 {
                     b.Property<int>("Id")
diff --git a/src/EllieBot/Migrations/Sqlite/20250317063300_init.cs b/src/EllieBot/Migrations/Sqlite/20250318222152_init.cs
similarity index 99%
rename from src/EllieBot/Migrations/Sqlite/20250317063300_init.cs
rename to src/EllieBot/Migrations/Sqlite/20250318222152_init.cs
index 73c5d84..429b5fb 100644
--- a/src/EllieBot/Migrations/Sqlite/20250317063300_init.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250318222152_init.cs
@@ -1181,6 +1181,22 @@ namespace EllieBot.Migrations.Sqlite
                     table.PrimaryKey("PK_Warnings", x => x.Id);
                 });
 
+            migrationBuilder.CreateTable(
+                name: "XpExcludedItem",
+                columns: table => new
+                {
+                    Id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
+                    ItemType = table.Column<int>(type: "INTEGER", nullable: false),
+                    ItemId = table.Column<ulong>(type: "INTEGER", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_XpExcludedItem", x => x.Id);
+                    table.UniqueConstraint("AK_XpExcludedItem_GuildId_ItemType_ItemId", x => new { x.GuildId, x.ItemType, x.ItemId });
+                });
+
             migrationBuilder.CreateTable(
                 name: "XpSettings",
                 columns: table => new
@@ -2282,6 +2298,11 @@ namespace EllieBot.Migrations.Sqlite
                 table: "XpCurrencyReward",
                 column: "XpSettingsId");
 
+            migrationBuilder.CreateIndex(
+                name: "IX_XpExcludedItem_GuildId",
+                table: "XpExcludedItem",
+                column: "GuildId");
+
             migrationBuilder.CreateIndex(
                 name: "IX_XpRoleReward_XpSettingsId_Level",
                 table: "XpRoleReward",
@@ -2576,6 +2597,9 @@ namespace EllieBot.Migrations.Sqlite
             migrationBuilder.DropTable(
                 name: "XpCurrencyReward");
 
+            migrationBuilder.DropTable(
+                name: "XpExcludedItem");
+
             migrationBuilder.DropTable(
                 name: "XpRoleReward");
 
diff --git a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
index 0792778..6ef86fe 100644
--- a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
@@ -2425,6 +2425,30 @@ namespace EllieBot.Migrations.Sqlite
                     b.ToTable("XpCurrencyReward");
                 });
 
+            modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<ulong>("GuildId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<ulong>("ItemId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ItemType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasAlternateKey("GuildId", "ItemType", "ItemId");
+
+                    b.HasIndex("GuildId");
+
+                    b.ToTable("XpExcludedItem");
+                });
+
             modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
                 {
                     b.Property<int>("Id")
diff --git a/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionCommands.cs b/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionCommands.cs
new file mode 100644
index 0000000..43a4624
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionCommands.cs
@@ -0,0 +1,84 @@
+using EllieBot.Db.Models;
+
+namespace EllieBot.Modules.Xp;
+
+public partial class Xp
+{
+    [RequireUserPermission(GuildPermission.Administrator)]
+    public class XpExclusionCommands : EllieModule<XpExclusionService>
+    {
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        [UserPerm(GuildPerm.Administrator)]
+        public async Task XpExclusion()
+        {
+            var exclusions = await _service.GetExclusionsAsync(ctx.Guild.Id);
+
+            if (!exclusions.Any())
+            {
+                await Response().Pending(strs.xp_exclusion_none).SendAsync();
+                return;
+            }
+
+            await Response()
+                .Paginated()
+                .Items(exclusions.OrderBy(x => x.ItemType).ToList())
+                .PageSize(10)
+                .Page((items, _) =>
+                {
+                    var eb = CreateEmbed()
+                        .WithOkColor()
+                        .WithTitle(GetText(strs.xp_exclusion_title));
+
+                    foreach (var item in items)
+                    {
+                        var itemType = item.ItemType;
+                        var mention = GetMention(itemType, item.ItemId);
+
+                        eb.AddField(itemType.ToString(), mention);
+                    }
+
+                    return eb;
+                })
+                .SendAsync();
+        }
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        [UserPerm(GuildPerm.Administrator)]
+        public async Task XpExclude([Leftover] IRole role)
+            => await XpExclude(XpExcludedItemType.Role, role.Id);
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        [UserPerm(GuildPerm.Administrator)]
+        public async Task XpExclude([Leftover] IUser user)
+            => await XpExclude(XpExcludedItemType.User, user.Id);
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        [UserPerm(GuildPerm.Administrator)]
+        public async Task XpExclude(XpExcludedItemType type, ulong itemId)
+        {
+            var isExcluded = await _service.ToggleExclusionAsync(ctx.Guild.Id, type, itemId);
+
+            if (isExcluded)
+                await Response()
+                    .Confirm(strs.xp_exclude_added(type.ToString(), GetMention(type, itemId)))
+                    .SendAsync();
+            else
+                await Response()
+                    .Confirm(strs.xp_exclude_removed(type.ToString(), GetMention(type, itemId)))
+                    .SendAsync();
+        }
+
+        private string GetMention(XpExcludedItemType itemType, ulong itemId)
+            => itemType switch
+            {
+                XpExcludedItemType.Role => ctx.Guild.GetRole(itemId)?.ToString() ?? itemId.ToString(),
+                XpExcludedItemType.User => (ctx.Guild as SocketGuild)?.GetUser(itemId)?.ToString() ??
+                                           itemId.ToString(),
+                _ => itemId.ToString()
+            };
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionService.cs b/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionService.cs
new file mode 100644
index 0000000..e12375c
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpExclusion/XpExclusionService.cs
@@ -0,0 +1,99 @@
+using LinqToDB.EntityFrameworkCore;
+using EllieBot.Common.ModuleBehaviors;
+using EllieBot.Db.Models;
+using LinqToDB;
+using Microsoft.EntityFrameworkCore;
+
+namespace EllieBot.Modules.Xp;
+
+public class XpExclusionService(DbService db, ShardData shardData) : IReadyExecutor, IEService
+{
+    private ConcurrentHashSet<(ulong GuildId, XpExcludedItemType ItemType, ulong ItemId)> _exclusions = new();
+
+    public async Task OnReadyAsync()
+    {
+        await using var uow = db.GetDbContext();
+        _exclusions = await uow.GetTable<XpExcludedItem>()
+            .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
+            .ToListAsyncLinqToDB()
+            .Fmap(x => x
+                .Select(x => (x.GuildId, x.ItemType, x.ItemId))
+                .ToHashSet()
+                .ToConcurrentSet());
+    }
+
+    /// <summary>
+    /// Toggles exclusion for the specified item. If the item was excluded, it will be included
+    /// and vice versa.
+    /// </summary>
+    /// <param name="guildId">ID of the guild</param>
+    /// <param name="itemType">Type of the item to toggle exclusion for</param>
+    /// <param name="itemId">ID of the item to toggle exclusion for</param>
+    /// <returns>True if the item is now excluded, false if it's no longer excluded</returns>
+    public async Task<bool> ToggleExclusionAsync(ulong guildId, XpExcludedItemType itemType, ulong itemId)
+    {
+        var key = (guildId, itemType, itemId);
+        var isExcluded = false;
+
+        await using var uow = db.GetDbContext();
+        if (_exclusions.Contains(key))
+        {
+            isExcluded = false;
+            // item exists, remove it
+            await uow.GetTable<XpExcludedItem>()
+                .Where(x => x.GuildId == guildId
+                       && x.ItemType == itemType
+                       && x.ItemId == itemId)
+                .DeleteAsync();
+
+            _exclusions.TryRemove(key);
+        }
+        else
+        {
+            isExcluded = true;
+            // item doesn't exist, add it
+            await uow.GetTable<XpExcludedItem>()
+                .InsertOrUpdateAsync(() => new XpExcludedItem
+                {
+                    GuildId = guildId,
+                    ItemType = itemType,
+                    ItemId = itemId
+                },
+                _ => new(),
+                () => new XpExcludedItem
+                {
+                    GuildId = guildId,
+                    ItemType = itemType,
+                    ItemId = itemId
+                });
+
+            _exclusions.Add(key);
+        }
+
+        return isExcluded;
+    }
+
+    /// <summary>
+    /// Gets a list of all excluded items for a guild.
+    /// </summary>
+    /// <param name="guildId">ID of the guild</param>
+    /// <returns>List of excluded items in the guild</returns>
+    public async Task<IReadOnlyList<XpExcludedItem>> GetExclusionsAsync(ulong guildId)
+    {
+        await using var uow = db.GetDbContext();
+        return await uow.GetTable<XpExcludedItem>()
+            .AsNoTracking()
+            .Where(x => x.GuildId == guildId)
+            .ToListAsyncLinqToDB();
+    }
+
+    /// <summary>
+    /// Checks if the specified item is excluded from XP gain.
+    /// </summary>
+    /// <param name="guildId">ID of the guild</param>
+    /// <param name="itemType">Type of the item</param>
+    /// <param name="itemId">ID of the item</param>
+    /// <returns>True if the item is excluded, otherwise false</returns>
+    public bool IsExcluded(ulong guildId, XpExcludedItemType itemType, ulong itemId)
+        => _exclusions.Contains((guildId, itemType, itemId));
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs
index 9b08527..495c7d5 100644
--- a/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs
+++ b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs
@@ -9,7 +9,7 @@ namespace EllieBot.Modules.Xp;
 
 using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates);
 
-public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService
+public class XpRateService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService
 {
     private ConcurrentDictionary<(XpRateType RateType, ulong GuildId), XpRate> _guildRates = new();
     private ConcurrentDictionary<ulong, ConcurrentDictionary<(XpRateType, ulong), XpRate>> _channelRates = new();
diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
index 4cb471a..7511cbd 100644
--- a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
+++ b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
@@ -3,7 +3,7 @@ namespace EllieBot.Modules.Xp;
 public partial class Xp
 {
     [RequireUserPermission(GuildPermission.ManageGuild)]
-    public class XpRateCommands : EllieModule<GuildConfigXpService>
+    public class XpRateCommands : EllieModule<XpRateService>
     {
         [Cmd]
         [RequireContext(ContextType.Guild)]
diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs
index a3c3fcc..b371299 100644
--- a/src/EllieBot/Modules/Xp/XpService.cs
+++ b/src/EllieBot/Modules/Xp/XpService.cs
@@ -40,7 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     private readonly INotifySubscriber _notifySub;
     private readonly IMemoryCache _memCache;
     private readonly XpTemplateService _templateService;
-    private readonly GuildConfigXpService _xpRateService;
+    private readonly XpRateService _xpRateRateService;
+    private readonly XpExclusionService _xpExcl;
 
     private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
 
@@ -60,7 +61,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         IMemoryCache memCache,
         ShardData shardData,
         XpTemplateService templateService,
-        GuildConfigXpService xpRateService)
+        XpRateService xpRateRateService,
+        XpExclusionService xpExcl
+    )
     {
         _db = db;
         _images = images;
@@ -72,7 +75,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         _notifySub = notifySub;
         _memCache = memCache;
         _templateService = templateService;
-        _xpRateService = xpRateService;
+        _xpRateRateService = xpRateRateService;
+        _xpExcl = xpExcl;
         _client = client;
         _ps = ps;
         _c = c;
@@ -143,7 +147,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                 if (!IsVoiceChannelActive(vc))
                     continue;
 
-                var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
+                var rate = _xpRateRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
 
                 if (rate.IsExcluded())
                     continue;
@@ -153,6 +157,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                     if (!UserParticipatingInVoiceChannel(u))
                         continue;
 
+                    if (IsUserExcluded(g, u))
+                        continue;
+
                     if (oldBatch.Contains(u))
                     {
                         validUsers.Add(new(u, rate.Amount, vc.Id));
@@ -509,15 +516,18 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
         _ = Task.Run(async () =>
         {
+            if (IsUserExcluded(guild, user))
+                return;
+
             var isImage = arg.Attachments.Any(a => a.Height >= 16 && a.Width >= 16);
             var isText = arg.Content.Contains(' ') || arg.Content.Length >= 5;
 
-            var textRate = _xpRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id);
+            var textRate = _xpRateRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id);
 
             XpRate rate;
             if (isImage)
             {
-                var imageRate = _xpRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
+                var imageRate = _xpRateRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
                 if (imageRate.IsExcluded())
                     return;
 
@@ -544,6 +554,20 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         return Task.CompletedTask;
     }
 
+    private bool IsUserExcluded(IGuild guild, SocketGuildUser user)
+    {
+        if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.User, user.Id))
+            return true;
+
+        foreach (var role in user.Roles)
+        {
+            if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.Role, role.Id))
+                return true;
+        }
+
+        return false;
+    }
+
     public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
     {
         await using var ctx = _db.GetDbContext();
@@ -614,49 +638,54 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
             }
 
             var avatarUrl = stats.User.RealAvatarUrl();
-            byte[] avatarImageData = null;
-
-            if (avatarUrl is not null)
+            var avatarFetchTask = Task.Run(async () =>
             {
-                var result = await _c.GetImageDataAsync(avatarUrl);
-                if (!result.TryPickT0(out avatarImageData, out _))
+                try
                 {
-                    using (var http = _httpFactory.CreateClient())
-                    {
-                        var avatarData = await http.GetByteArrayAsync(avatarUrl);
-                        using (var tempDraw = Image.Load<Rgba32>(avatarData))
-                        {
-                            tempDraw.Mutate(x => x
-                                .Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
-                                .ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
-                                                         template.User.Icon.Size.Y)
-                                                     / 2.0f));
-                            await using (var stream = await tempDraw.ToStreamAsync())
-                            {
-                                avatarImageData = stream.ToArray();
-                            }
-                        }
-                    }
+                    if (avatarUrl is null)
+                        return null;
 
-                    await _c.SetImageDataAsync(avatarUrl, avatarImageData);
+                    var result = await _c.GetImageDataAsync(avatarUrl);
+                    if (result.TryPickT0(out var imgData, out _))
+                        return imgData;
+
+                    using var http = _httpFactory.CreateClient();
+
+                    var avatarData = await http.GetByteArrayAsync(avatarUrl);
+                    using var tempDraw = Image.Load<Rgba32>(avatarData);
+
+                    tempDraw.Mutate(x => x
+                        .Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
+                        .ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
+                                                 template.User.Icon.Size.Y)
+                                             / 2.0f));
+                    await using var stream = await tempDraw.ToStreamAsync();
+                    var data = stream.ToArray();
+                    await _c.SetImageDataAsync(avatarUrl, data);
+                    return data;
                 }
-            }
+                catch (Exception)
+                {
+                    return null;
+                }
+            });
 
             using var img = Image.Load<Rgba32>(bgBytes);
 
-            if (template.User.Name.Show)
+
+            img.Mutate(x =>
             {
-                var fontSize = (int)(template.User.Name.FontSize * 0.9);
-                var username = stats.User.ToString();
-                var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
-
-                var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
-                var scale = 400f / size.Width;
-                if (scale < 1)
-                    usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
-
-                img.Mutate(x =>
+                if (template.User.Name.Show)
                 {
+                    var fontSize = (int)(template.User.Name.FontSize * 0.9);
+                    var username = stats.User.ToString();
+                    var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
+
+                    var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
+                    var scale = 400f / size.Width;
+                    if (scale < 1)
+                        usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
+
                     x.DrawText(new RichTextOptions(usernameFont)
                     {
                         HorizontalAlignment = HorizontalAlignment.Left,
@@ -666,126 +695,129 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                     },
                         "@" + username,
                         Brushes.Solid(template.User.Name.Color));
+                }
 
 
-                    //club name
+                //club name
 
-                    if (template.Club.Name.Show)
+                if (template.Club.Name.Show)
+                {
+                    var clubName = stats.User.Club?.ToString() ?? "-";
+
+                    var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
+
+                    x.DrawText(new RichTextOptions(clubFont)
                     {
-                        var clubName = stats.User.Club?.ToString() ?? "-";
+                        HorizontalAlignment = HorizontalAlignment.Right,
+                        VerticalAlignment = VerticalAlignment.Top,
+                        FallbackFontFamilies = _fonts.FallBackFonts,
+                        Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
+                    },
+                        clubName,
+                        Brushes.Solid(template.Club.Name.Color));
+                }
 
-                        var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
+                Font GetTruncatedFont(
+                    FontFamily fontFamily,
+                    int fontSize,
+                    FontStyle style,
+                    string text,
+                    int maxSize)
+                {
+                    var font = fontFamily.CreateFont(fontSize, style);
+                    var size = TextMeasurer.MeasureSize(text, new(font));
+                    var scale = maxSize / size.Width;
+                    if (scale < 1)
+                        font = fontFamily.CreateFont(fontSize * scale, style);
 
-                        x.DrawText(new RichTextOptions(clubFont)
+                    return font;
+                }
+
+
+                if (template.User.Level.Show)
+                {
+                    var guildLevelFont = GetTruncatedFont(
+                        _fonts.NotoSans,
+                        template.User.Level.FontSize,
+                        FontStyle.Bold,
+                        stats.Guild.Level.ToString(),
+                        33);
+
+
+                    x.DrawText(stats.Guild.Level.ToString(),
+                        guildLevelFont,
+                        template.User.Level.Color,
+                        new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
+                }
+
+
+                var guild = stats.Guild;
+
+                //xp bar
+                if (template.User.Xp.Bar.Show)
+                {
+                    var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
+                    DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
+                }
+
+                if (template.User.Xp.Guild.Show)
+                {
+                    x.DrawText(
+                        new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
+                            FontStyle.Bold))
                         {
-                            HorizontalAlignment = HorizontalAlignment.Right,
-                            VerticalAlignment = VerticalAlignment.Top,
-                            FallbackFontFamilies = _fonts.FallBackFonts,
-                            Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
+                            HorizontalAlignment = HorizontalAlignment.Center,
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
                         },
-                            clubName,
-                            Brushes.Solid(template.Club.Name.Color));
-                    }
+                        $"{guild.LevelXp}/{guild.RequiredXp}",
+                        Brushes.Solid(template.User.Xp.Guild.Color));
+                }
 
-                    Font GetTruncatedFont(
-                        FontFamily fontFamily,
-                        int fontSize,
-                        FontStyle style,
-                        string text,
-                        int maxSize)
-                    {
-                        var font = fontFamily.CreateFont(fontSize, style);
-                        var size = TextMeasurer.MeasureSize(text, new(font));
-                        var scale = maxSize / size.Width;
-                        if (scale < 1)
-                            font = fontFamily.CreateFont(fontSize * scale, style);
+                var rankPen = new SolidPen(Color.White, 1);
+                //ranking
 
-                        return font;
-                    }
+                if (template.User.Rank.Show)
+                {
+                    var guildRankStr = stats.GuildRanking.ToString();
 
+                    var guildRankFont = GetTruncatedFont(
+                        _fonts.NotoSans,
+                        template.User.Rank.FontSize,
+                        FontStyle.Bold,
+                        guildRankStr,
+                        22);
 
-                    if (template.User.Level.Show)
-                    {
-                        var guildLevelFont = GetTruncatedFont(
-                            _fonts.NotoSans,
-                            template.User.Level.FontSize,
-                            FontStyle.Bold,
-                            stats.Guild.Level.ToString(),
-                            33);
-
-
-                        x.DrawText(stats.Guild.Level.ToString(),
-                            guildLevelFont,
-                            template.User.Level.Color,
-                            new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
-                    }
-
-
-                    var guild = stats.Guild;
-
-                    //xp bar
-                    if (template.User.Xp.Bar.Show)
-                    {
-                        var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
-                        DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
-                    }
-
-                    if (template.User.Xp.Guild.Show)
-                    {
-                        x.DrawText(
-                            new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
-                                FontStyle.Bold))
-                            {
-                                HorizontalAlignment = HorizontalAlignment.Center,
-                                VerticalAlignment = VerticalAlignment.Center,
-                                Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
-                            },
-                            $"{guild.LevelXp}/{guild.RequiredXp}",
-                            Brushes.Solid(template.User.Xp.Guild.Color));
-                    }
-
-                    var rankPen = new SolidPen(Color.White, 1);
-                    //ranking
-
-                    if (template.User.Rank.Show)
-                    {
-                        var guildRankStr = stats.GuildRanking.ToString();
-
-                        var guildRankFont = GetTruncatedFont(
-                            _fonts.NotoSans,
-                            template.User.Rank.FontSize,
-                            FontStyle.Bold,
-                            guildRankStr,
-                            22);
-
-                        x.DrawText(
-                            new RichTextOptions(guildRankFont)
-                            {
-                                Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
-                            },
-                            guildRankStr,
-                            Brushes.Solid(template.User.Rank.Color),
-                            rankPen
-                        );
-                    }
-
-                    if (template.User.Icon.Show)
-                    {
-                        try
+                    x.DrawText(
+                        new RichTextOptions(guildRankFont)
                         {
-                            using var toDraw = Image.Load(avatarImageData);
-                            if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
-                                toDraw.Mutate(x
-                                    => x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
+                            Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
+                        },
+                        guildRankStr,
+                        Brushes.Solid(template.User.Rank.Color),
+                        rankPen
+                    );
+                }
+            });
 
-                            x.DrawImage(toDraw,
-                                new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
-                                1);
-                        }
-                        catch (Exception ex)
-                        {
-                            Log.Warning(ex, "Error drawing avatar image");
-                        }
+            if (template.User.Icon.Show)
+            {
+                var avImageData = await avatarFetchTask;
+                img.Mutate(mut =>
+                {
+                    try
+                    {
+                        using var toDraw = Image.Load(avImageData);
+                        if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
+                            toDraw.Mutate(x => x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
+
+                        mut.DrawImage(toDraw,
+                            new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
+                            1);
+                    }
+                    catch (Exception ex)
+                    {
+                        Log.Warning(ex, "Error drawing avatar image");
                     }
                 });
             }
diff --git a/src/EllieBot/strings/aliases.yml b/src/EllieBot/strings/aliases.yml
index 1ba0dc7..1e39a10 100644
--- a/src/EllieBot/strings/aliases.yml
+++ b/src/EllieBot/strings/aliases.yml
@@ -1626,4 +1626,10 @@ scheduledelete:
 scheduleadd:
    - scheduleadd
    - scha
-   - schadd
\ No newline at end of file
+   - schadd
+xpexclusion:
+   - xpexclusion
+   - xpexl
+xpexclude:
+   - xpexclude
+   - xpex
\ No newline at end of file
diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml
index 3c17d20..a920bb4 100644
--- a/src/EllieBot/strings/commands/commands.en-US.yml
+++ b/src/EllieBot/strings/commands/commands.en-US.yml
@@ -1854,7 +1854,7 @@ playlistload:
     - 5
   params:
     - id:
-        desc: "The id of the playlist to be loaded."        
+        desc: "The id of the playlist to be loaded."
 playlistsave:
   desc: Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.
   ex:
@@ -5100,4 +5100,25 @@ scheduleadd:
      - time:
          desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes"
      - command:
-         desc: "Command that will be executed after the specified time has elapsed"
\ No newline at end of file
+         desc: "Command that will be executed after the specified time has elapsed"
+xpexclusion:
+   desc: |-
+     Shows a list of all XP exclusions in the server.
+   ex:
+     - ''
+   params:
+     - { } 
+xpexclude:
+   desc: |-
+     Toggles XP gain exclusion for a specified item.
+     Item types can be Role or User.
+   ex:
+     - '@CoolRole'
+     - '@User'
+     - 'role 123123123'
+     - 'user 123123123'
+   params:
+     - type:
+         desc: "Type of the item to exclude: role or user"
+       itemId:
+         desc: "ID or mention of the item to exclude."
\ 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 e28be28..1427c2a 100644
--- a/src/EllieBot/strings/responses/responses.en-US.json
+++ b/src/EllieBot/strings/responses/responses.en-US.json
@@ -1217,5 +1217,9 @@
   "schedule_command": "Command",
   "schedule_when": "Executes At",
   "schedule_add_success": "Scheduled command successfully added.",
-  "schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more."
+  "schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more.",
+  "xp_exclusion_none": "There are no exclusions set for this server.",
+  "xp_exclusion_title": "XP Exclusions",
+  "xp_exclude_added": "{0}: {1} has been excluded from the XP system.",
+  "xp_exclude_removed": "{0}: {1} is no longer excluded from the XP system."
 }
\ No newline at end of file