diff --git a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
index a8f9329..cf8bfd8 100644
--- a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
+++ b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
@@ -121,7 +121,6 @@ public static class GuildConfigExtensions
             .InsertWithOutputAsync(() => new()
             {
                 GuildId = guildId,
-                ServerExcluded = false,
             });
 
         return srs;
diff --git a/src/EllieBot/Db/Models/xp/UserXpStats.cs b/src/EllieBot/Db/Models/xp/UserXpStats.cs
index df39e14..0032c01 100644
--- a/src/EllieBot/Db/Models/xp/UserXpStats.cs
+++ b/src/EllieBot/Db/Models/xp/UserXpStats.cs
@@ -1,4 +1,3 @@
-#nullable disable
 namespace EllieBot.Db.Models;
 
 public class UserXpStats : DbEntity
diff --git a/src/EllieBot/Db/Models/xp/XpSettings.cs b/src/EllieBot/Db/Models/xp/XpSettings.cs
index f5af7e9..aa9aaef 100644
--- a/src/EllieBot/Db/Models/xp/XpSettings.cs
+++ b/src/EllieBot/Db/Models/xp/XpSettings.cs
@@ -1,14 +1,9 @@
 #nullable disable
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
 namespace EllieBot.Db.Models;
 
 public class XpSettings : DbEntity
 {
     public ulong GuildId { get; set; }
-    public bool ServerExcluded { get; set; }
-    public HashSet<ExcludedItem> ExclusionList { get; set; } = new();
 
     public HashSet<XpRoleReward> RoleRewards { get; set; } = new();
     public HashSet<XpCurrencyReward> CurrencyRewards { get; set; } = new();
diff --git a/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs b/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs
index a5fd6b8..83b5621 100644
--- a/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs
+++ b/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs
@@ -15,8 +15,5 @@ public class XpSettingsEntityConfiguration : IEntityTypeConfiguration<XpSettings
         
         builder.HasMany(x => x.RoleRewards)
                .WithOne();
-
-        builder.HasMany(x => x.ExclusionList)
-               .WithOne();
     }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql b/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql
new file mode 100644
index 0000000..e810882
--- /dev/null
+++ b/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql
@@ -0,0 +1,31 @@
+START TRANSACTION;
+DROP TABLE excludeditem;
+
+ALTER TABLE guildxpconfig DROP CONSTRAINT pk_guildxpconfig;
+
+ALTER TABLE channelxpconfig DROP CONSTRAINT ak_channelxpconfig_guildid_channelid;
+
+ALTER TABLE xpsettings DROP COLUMN serverexcluded;
+
+ALTER TABLE guildxpconfig ALTER COLUMN xpamount TYPE bigint;
+
+ALTER TABLE guildxpconfig ALTER COLUMN cooldown TYPE real;
+
+ALTER TABLE guildxpconfig ADD id integer GENERATED BY DEFAULT AS IDENTITY;
+
+ALTER TABLE guildxpconfig ADD ratetype integer NOT NULL DEFAULT 0;
+
+ALTER TABLE channelxpconfig ALTER COLUMN xpamount TYPE bigint;
+
+ALTER TABLE channelxpconfig ADD ratetype integer NOT NULL DEFAULT 0;
+
+ALTER TABLE guildxpconfig ADD CONSTRAINT ak_guildxpconfig_guildid_ratetype UNIQUE (guildid, ratetype);
+
+ALTER TABLE guildxpconfig ADD CONSTRAINT pk_guildxpconfig PRIMARY KEY (id);
+
+ALTER TABLE channelxpconfig ADD CONSTRAINT ak_channelxpconfig_guildid_channelid_ratetype UNIQUE (guildid, channelid, ratetype);
+
+INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
+VALUES ('20250226215159_xp-rate-rework', '9.0.1');
+
+COMMIT;
diff --git a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs
similarity index 98%
rename from src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs
rename to src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs
index 66f7870..4bc0e7f 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs
@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace EllieBot.Migrations.PostgreSql
 {
     [DbContext(typeof(PostgreSqlContext))]
-    [Migration("20250225212209_init")]
+    [Migration("20250226215222_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -877,40 +877,6 @@ namespace EllieBot.Migrations.PostgreSql
                 b.ToTable("discorduser", (string)null);
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.Property<int>("Id")
-                    .ValueGeneratedOnAdd()
-                    .HasColumnType("integer")
-                    .HasColumnName("id");
-
-                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                b.Property<DateTime?>("DateAdded")
-                    .HasColumnType("timestamp without time zone")
-                    .HasColumnName("dateadded");
-
-                b.Property<decimal>("ItemId")
-                    .HasColumnType("numeric(20,0)")
-                    .HasColumnName("itemid");
-
-                b.Property<int>("ItemType")
-                    .HasColumnType("integer")
-                    .HasColumnName("itemtype");
-
-                b.Property<int?>("XpSettingsId")
-                    .HasColumnType("integer")
-                    .HasColumnName("xpsettingsid");
-
-                b.HasKey("Id")
-                    .HasName("pk_excludeditem");
-
-                b.HasIndex("XpSettingsId")
-                    .HasDatabaseName("ix_excludeditem_xpsettingsid");
-
-                b.ToTable("excludeditem", (string)null);
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
             {
                 b.Property<int>("Id")
@@ -3359,10 +3325,6 @@ namespace EllieBot.Migrations.PostgreSql
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<bool>("ServerExcluded")
-                    .HasColumnType("boolean")
-                    .HasColumnName("serverexcluded");
-
                 b.HasKey("Id")
                     .HasName("pk_xpsettings");
 
@@ -3503,41 +3465,58 @@ namespace EllieBot.Migrations.PostgreSql
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
                     .HasColumnType("integer")
+                    .HasColumnName("ratetype");
+
+                b.Property<long>("XpAmount")
+                    .HasColumnType("bigint")
                     .HasColumnName("xpamount");
 
                 b.HasKey("Id")
                     .HasName("pk_channelxpconfig");
 
-                b.HasAlternateKey("GuildId", "ChannelId")
-                    .HasName("ak_channelxpconfig_guildid_channelid");
+                b.HasAlternateKey("GuildId", "ChannelId", "RateType")
+                    .HasName("ak_channelxpconfig_guildid_channelid_ratetype");
 
                 b.ToTable("channelxpconfig", (string)null);
             });
 
             modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
             {
-                b.Property<decimal>("GuildId")
+                b.Property<int>("Id")
                     .ValueGeneratedOnAdd()
+                    .HasColumnType("integer")
+                    .HasColumnName("id");
+
+                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("real")
+                    .HasColumnName("cooldown");
+
+                b.Property<decimal>("GuildId")
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<int>("Cooldown")
+                b.Property<int>("RateType")
                     .HasColumnType("integer")
-                    .HasColumnName("cooldown");
+                    .HasColumnName("ratetype");
 
-                b.Property<int>("XpAmount")
-                    .HasColumnType("integer")
+                b.Property<long>("XpAmount")
+                    .HasColumnType("bigint")
                     .HasColumnName("xpamount");
 
                 b.Property<string>("XpTemplateUrl")
                     .HasColumnType("text")
                     .HasColumnName("xptemplateurl");
 
-                b.HasKey("GuildId")
+                b.HasKey("Id")
                     .HasName("pk_guildxpconfig");
 
+                b.HasAlternateKey("GuildId", "RateType")
+                    .HasName("ak_guildxpconfig_guildid_ratetype");
+
                 b.ToTable("guildxpconfig", (string)null);
             });
 
@@ -3744,14 +3723,6 @@ namespace EllieBot.Migrations.PostgreSql
                 b.Navigation("Club");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.HasOne("EllieBot.Db.Models.XpSettings", null)
-                    .WithMany("ExclusionList")
-                    .HasForeignKey("XpSettingsId")
-                    .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b =>
             {
                 b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null)
@@ -4038,8 +4009,6 @@ namespace EllieBot.Migrations.PostgreSql
             {
                 b.Navigation("CurrencyRewards");
 
-                b.Navigation("ExclusionList");
-
                 b.Navigation("RoleRewards");
             });
 #pragma warning restore 612, 618
diff --git a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs
similarity index 98%
rename from src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs
rename to src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs
index bf7ff89..9d717f0 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs
@@ -193,15 +193,16 @@ namespace EllieBot.Migrations.PostgreSql
                 {
                     id = table.Column<int>(type: "integer", nullable: false)
                         .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    ratetype = table.Column<int>(type: "integer", nullable: false),
                     guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
                     channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    xpamount = table.Column<int>(type: "integer", nullable: false),
+                    xpamount = table.Column<long>(type: "bigint", nullable: false),
                     cooldown = table.Column<float>(type: "real", nullable: false)
                 },
                 constraints: table =>
                 {
                     table.PrimaryKey("pk_channelxpconfig", x => x.id);
-                    table.UniqueConstraint("ak_channelxpconfig_guildid_channelid", x => new { x.guildid, x.channelid });
+                    table.UniqueConstraint("ak_channelxpconfig_guildid_channelid_ratetype", x => new { x.guildid, x.channelid, x.ratetype });
                 });
 
             migrationBuilder.CreateTable(
@@ -508,14 +509,18 @@ namespace EllieBot.Migrations.PostgreSql
                 name: "guildxpconfig",
                 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),
-                    xpamount = table.Column<int>(type: "integer", nullable: false),
-                    cooldown = table.Column<int>(type: "integer", nullable: false),
+                    ratetype = table.Column<int>(type: "integer", nullable: false),
+                    xpamount = table.Column<long>(type: "bigint", nullable: false),
+                    cooldown = table.Column<float>(type: "real", nullable: false),
                     xptemplateurl = table.Column<string>(type: "text", nullable: true)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("pk_guildxpconfig", x => x.guildid);
+                    table.PrimaryKey("pk_guildxpconfig", x => x.id);
+                    table.UniqueConstraint("ak_guildxpconfig_guildid_ratetype", x => new { x.guildid, x.ratetype });
                 });
 
             migrationBuilder.CreateTable(
@@ -1151,7 +1156,6 @@ namespace EllieBot.Migrations.PostgreSql
                     id = table.Column<int>(type: "integer", nullable: false)
                         .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                     guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    serverexcluded = table.Column<bool>(type: "boolean", nullable: false),
                     dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
                 },
                 constraints: table =>
@@ -1506,27 +1510,6 @@ namespace EllieBot.Migrations.PostgreSql
                         onDelete: ReferentialAction.Cascade);
                 });
 
-            migrationBuilder.CreateTable(
-                name: "excludeditem",
-                columns: table => new
-                {
-                    id = table.Column<int>(type: "integer", nullable: false)
-                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
-                    xpsettingsid = table.Column<int>(type: "integer", nullable: true),
-                    itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    itemtype = table.Column<int>(type: "integer", nullable: false),
-                    dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("pk_excludeditem", x => x.id);
-                    table.ForeignKey(
-                        name: "fk_excludeditem_xpsettings_xpsettingsid",
-                        column: x => x.xpsettingsid,
-                        principalTable: "xpsettings",
-                        principalColumn: "id");
-                });
-
             migrationBuilder.CreateTable(
                 name: "xpcurrencyreward",
                 columns: table => new
@@ -1860,11 +1843,6 @@ namespace EllieBot.Migrations.PostgreSql
                 table: "discorduser",
                 column: "username");
 
-            migrationBuilder.CreateIndex(
-                name: "ix_excludeditem_xpsettingsid",
-                table: "excludeditem",
-                column: "xpsettingsid");
-
             migrationBuilder.CreateIndex(
                 name: "ix_feedsub_guildid_url",
                 table: "feedsub",
@@ -2367,9 +2345,6 @@ namespace EllieBot.Migrations.PostgreSql
             migrationBuilder.DropTable(
                 name: "discordpermoverrides");
 
-            migrationBuilder.DropTable(
-                name: "excludeditem");
-
             migrationBuilder.DropTable(
                 name: "expressions");
 
diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
index d58940a..5c0c910 100644
--- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
@@ -874,40 +874,6 @@ namespace EllieBot.Migrations.PostgreSql
                 b.ToTable("discorduser", (string)null);
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.Property<int>("Id")
-                    .ValueGeneratedOnAdd()
-                    .HasColumnType("integer")
-                    .HasColumnName("id");
-
-                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                b.Property<DateTime?>("DateAdded")
-                    .HasColumnType("timestamp without time zone")
-                    .HasColumnName("dateadded");
-
-                b.Property<decimal>("ItemId")
-                    .HasColumnType("numeric(20,0)")
-                    .HasColumnName("itemid");
-
-                b.Property<int>("ItemType")
-                    .HasColumnType("integer")
-                    .HasColumnName("itemtype");
-
-                b.Property<int?>("XpSettingsId")
-                    .HasColumnType("integer")
-                    .HasColumnName("xpsettingsid");
-
-                b.HasKey("Id")
-                    .HasName("pk_excludeditem");
-
-                b.HasIndex("XpSettingsId")
-                    .HasDatabaseName("ix_excludeditem_xpsettingsid");
-
-                b.ToTable("excludeditem", (string)null);
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
             {
                 b.Property<int>("Id")
@@ -3356,10 +3322,6 @@ namespace EllieBot.Migrations.PostgreSql
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<bool>("ServerExcluded")
-                    .HasColumnType("boolean")
-                    .HasColumnName("serverexcluded");
-
                 b.HasKey("Id")
                     .HasName("pk_xpsettings");
 
@@ -3500,41 +3462,58 @@ namespace EllieBot.Migrations.PostgreSql
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
                     .HasColumnType("integer")
+                    .HasColumnName("ratetype");
+
+                b.Property<long>("XpAmount")
+                    .HasColumnType("bigint")
                     .HasColumnName("xpamount");
 
                 b.HasKey("Id")
                     .HasName("pk_channelxpconfig");
 
-                b.HasAlternateKey("GuildId", "ChannelId")
-                    .HasName("ak_channelxpconfig_guildid_channelid");
+                b.HasAlternateKey("GuildId", "ChannelId", "RateType")
+                    .HasName("ak_channelxpconfig_guildid_channelid_ratetype");
 
                 b.ToTable("channelxpconfig", (string)null);
             });
 
             modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
             {
-                b.Property<decimal>("GuildId")
+                b.Property<int>("Id")
                     .ValueGeneratedOnAdd()
+                    .HasColumnType("integer")
+                    .HasColumnName("id");
+
+                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("real")
+                    .HasColumnName("cooldown");
+
+                b.Property<decimal>("GuildId")
                     .HasColumnType("numeric(20,0)")
                     .HasColumnName("guildid");
 
-                b.Property<int>("Cooldown")
+                b.Property<int>("RateType")
                     .HasColumnType("integer")
-                    .HasColumnName("cooldown");
+                    .HasColumnName("ratetype");
 
-                b.Property<int>("XpAmount")
-                    .HasColumnType("integer")
+                b.Property<long>("XpAmount")
+                    .HasColumnType("bigint")
                     .HasColumnName("xpamount");
 
                 b.Property<string>("XpTemplateUrl")
                     .HasColumnType("text")
                     .HasColumnName("xptemplateurl");
 
-                b.HasKey("GuildId")
+                b.HasKey("Id")
                     .HasName("pk_guildxpconfig");
 
+                b.HasAlternateKey("GuildId", "RateType")
+                    .HasName("ak_guildxpconfig_guildid_ratetype");
+
                 b.ToTable("guildxpconfig", (string)null);
             });
 
@@ -3741,14 +3720,6 @@ namespace EllieBot.Migrations.PostgreSql
                 b.Navigation("Club");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.HasOne("EllieBot.Db.Models.XpSettings", null)
-                    .WithMany("ExclusionList")
-                    .HasForeignKey("XpSettingsId")
-                    .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b =>
             {
                 b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null)
@@ -4035,8 +4006,6 @@ namespace EllieBot.Migrations.PostgreSql
             {
                 b.Navigation("CurrencyRewards");
 
-                b.Navigation("ExclusionList");
-
                 b.Navigation("RoleRewards");
             });
 #pragma warning restore 612, 618
diff --git a/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql b/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql
new file mode 100644
index 0000000..371fe1a
--- /dev/null
+++ b/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql
@@ -0,0 +1,75 @@
+BEGIN TRANSACTION;
+DROP TABLE "ExcludedItem";
+
+ALTER TABLE "GuildXpConfig" ADD "Id" INTEGER NOT NULL DEFAULT 0;
+
+ALTER TABLE "GuildXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0;
+
+ALTER TABLE "ChannelXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0;
+
+CREATE TABLE "ef_temp_GuildXpConfig" (
+    "Id" INTEGER NOT NULL CONSTRAINT "PK_GuildXpConfig" PRIMARY KEY AUTOINCREMENT,
+    "Cooldown" REAL NOT NULL,
+    "GuildId" INTEGER NOT NULL,
+    "RateType" INTEGER NOT NULL,
+    "XpAmount" INTEGER NOT NULL,
+    "XpTemplateUrl" TEXT NULL,
+    CONSTRAINT "AK_GuildXpConfig_GuildId_RateType" UNIQUE ("GuildId", "RateType")
+);
+
+INSERT INTO "ef_temp_GuildXpConfig" ("Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl")
+SELECT "Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl"
+FROM "GuildXpConfig";
+
+CREATE TABLE "ef_temp_ChannelXpConfig" (
+    "Id" INTEGER NOT NULL CONSTRAINT "PK_ChannelXpConfig" PRIMARY KEY AUTOINCREMENT,
+    "ChannelId" INTEGER NOT NULL,
+    "Cooldown" REAL NOT NULL,
+    "GuildId" INTEGER NOT NULL,
+    "RateType" INTEGER NOT NULL,
+    "XpAmount" INTEGER NOT NULL,
+    CONSTRAINT "AK_ChannelXpConfig_GuildId_ChannelId_RateType" UNIQUE ("GuildId", "ChannelId", "RateType")
+);
+
+INSERT INTO "ef_temp_ChannelXpConfig" ("Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount")
+SELECT "Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount"
+FROM "ChannelXpConfig";
+
+CREATE TABLE "ef_temp_XpSettings" (
+    "Id" INTEGER NOT NULL CONSTRAINT "PK_XpSettings" PRIMARY KEY AUTOINCREMENT,
+    "DateAdded" TEXT NULL,
+    "GuildId" INTEGER NOT NULL
+);
+
+INSERT INTO "ef_temp_XpSettings" ("Id", "DateAdded", "GuildId")
+SELECT "Id", "DateAdded", "GuildId"
+FROM "XpSettings";
+
+COMMIT;
+
+PRAGMA foreign_keys = 0;
+
+BEGIN TRANSACTION;
+DROP TABLE "GuildXpConfig";
+
+ALTER TABLE "ef_temp_GuildXpConfig" RENAME TO "GuildXpConfig";
+
+DROP TABLE "ChannelXpConfig";
+
+ALTER TABLE "ef_temp_ChannelXpConfig" RENAME TO "ChannelXpConfig";
+
+DROP TABLE "XpSettings";
+
+ALTER TABLE "ef_temp_XpSettings" RENAME TO "XpSettings";
+
+COMMIT;
+
+PRAGMA foreign_keys = 1;
+
+BEGIN TRANSACTION;
+CREATE UNIQUE INDEX "IX_XpSettings_GuildId" ON "XpSettings" ("GuildId");
+
+COMMIT;
+
+INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
+VALUES ('20250226215156_xp-rate-rework', '9.0.1');
diff --git a/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs
similarity index 98%
rename from src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs
rename to src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs
index 483367d..18ebf28 100644
--- a/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs
@@ -11,7 +11,7 @@ using EllieBot.Db;
 namespace EllieBot.Migrations.Sqlite
 {
     [DbContext(typeof(SqliteContext))]
-    [Migration("20250225212206_init")]
+    [Migration("20250226215220_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -657,31 +657,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.ToTable("DiscordUser");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.Property<int>("Id")
-                    .ValueGeneratedOnAdd()
-                    .HasColumnType("INTEGER");
-
-                b.Property<DateTime?>("DateAdded")
-                    .HasColumnType("TEXT");
-
-                b.Property<ulong>("ItemId")
-                    .HasColumnType("INTEGER");
-
-                b.Property<int>("ItemType")
-                    .HasColumnType("INTEGER");
-
-                b.Property<int?>("XpSettingsId")
-                    .HasColumnType("INTEGER");
-
-                b.HasKey("Id");
-
-                b.HasIndex("XpSettingsId");
-
-                b.ToTable("ExcludedItem");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
             {
                 b.Property<int>("Id")
@@ -2499,9 +2474,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<bool>("ServerExcluded")
-                    .HasColumnType("INTEGER");
-
                 b.HasKey("Id");
 
                 b.HasIndex("GuildId")
@@ -2606,32 +2578,43 @@ namespace EllieBot.Migrations.Sqlite
                 b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
+                    .HasColumnType("INTEGER");
+
+                b.Property<long>("XpAmount")
                     .HasColumnType("INTEGER");
 
                 b.HasKey("Id");
 
-                b.HasAlternateKey("GuildId", "ChannelId");
+                b.HasAlternateKey("GuildId", "ChannelId", "RateType");
 
                 b.ToTable("ChannelXpConfig");
             });
 
             modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
             {
-                b.Property<ulong>("GuildId")
+                b.Property<int>("Id")
                     .ValueGeneratedOnAdd()
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("Cooldown")
+                b.Property<float>("Cooldown")
+                    .HasColumnType("REAL");
+
+                b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
+                    .HasColumnType("INTEGER");
+
+                b.Property<long>("XpAmount")
                     .HasColumnType("INTEGER");
 
                 b.Property<string>("XpTemplateUrl")
                     .HasColumnType("TEXT");
 
-                b.HasKey("GuildId");
+                b.HasKey("Id");
+
+                b.HasAlternateKey("GuildId", "RateType");
 
                 b.ToTable("GuildXpConfig");
             });
@@ -2803,13 +2786,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.Navigation("Club");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.HasOne("EllieBot.Db.Models.XpSettings", null)
-                    .WithMany("ExclusionList")
-                    .HasForeignKey("XpSettingsId");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b =>
             {
                 b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null)
@@ -3074,8 +3050,6 @@ namespace EllieBot.Migrations.Sqlite
             {
                 b.Navigation("CurrencyRewards");
 
-                b.Navigation("ExclusionList");
-
                 b.Navigation("RoleRewards");
             });
 #pragma warning restore 612, 618
diff --git a/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs b/src/EllieBot/Migrations/Sqlite/20250226215220_init.cs
similarity index 98%
rename from src/EllieBot/Migrations/Sqlite/20250225212206_init.cs
rename to src/EllieBot/Migrations/Sqlite/20250226215220_init.cs
index c18b049..566a6e2 100644
--- a/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250226215220_init.cs
@@ -192,15 +192,16 @@ namespace EllieBot.Migrations.Sqlite
                 {
                     Id = table.Column<int>(type: "INTEGER", nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
+                    RateType = table.Column<int>(type: "INTEGER", nullable: false),
                     GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
                     ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    XpAmount = table.Column<int>(type: "INTEGER", nullable: false),
+                    XpAmount = table.Column<long>(type: "INTEGER", nullable: false),
                     Cooldown = table.Column<float>(type: "REAL", nullable: false)
                 },
                 constraints: table =>
                 {
                     table.PrimaryKey("PK_ChannelXpConfig", x => x.Id);
-                    table.UniqueConstraint("AK_ChannelXpConfig_GuildId_ChannelId", x => new { x.GuildId, x.ChannelId });
+                    table.UniqueConstraint("AK_ChannelXpConfig_GuildId_ChannelId_RateType", x => new { x.GuildId, x.ChannelId, x.RateType });
                 });
 
             migrationBuilder.CreateTable(
@@ -507,15 +508,18 @@ namespace EllieBot.Migrations.Sqlite
                 name: "GuildXpConfig",
                 columns: table => new
                 {
-                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false)
+                    Id = table.Column<int>(type: "INTEGER", nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
-                    XpAmount = table.Column<int>(type: "INTEGER", nullable: false),
-                    Cooldown = table.Column<int>(type: "INTEGER", nullable: false),
+                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
+                    RateType = table.Column<int>(type: "INTEGER", nullable: false),
+                    XpAmount = table.Column<long>(type: "INTEGER", nullable: false),
+                    Cooldown = table.Column<float>(type: "REAL", nullable: false),
                     XpTemplateUrl = table.Column<string>(type: "TEXT", nullable: true)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_GuildXpConfig", x => x.GuildId);
+                    table.PrimaryKey("PK_GuildXpConfig", x => x.Id);
+                    table.UniqueConstraint("AK_GuildXpConfig_GuildId_RateType", x => new { x.GuildId, x.RateType });
                 });
 
             migrationBuilder.CreateTable(
@@ -1154,7 +1158,6 @@ namespace EllieBot.Migrations.Sqlite
                     Id = table.Column<int>(type: "INTEGER", nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
                     GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    ServerExcluded = table.Column<bool>(type: "INTEGER", nullable: false),
                     DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
                 },
                 constraints: table =>
@@ -1509,27 +1512,6 @@ namespace EllieBot.Migrations.Sqlite
                         onDelete: ReferentialAction.Cascade);
                 });
 
-            migrationBuilder.CreateTable(
-                name: "ExcludedItem",
-                columns: table => new
-                {
-                    Id = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    XpSettingsId = table.Column<int>(type: "INTEGER", nullable: true),
-                    ItemId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    ItemType = table.Column<int>(type: "INTEGER", nullable: false),
-                    DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_ExcludedItem", x => x.Id);
-                    table.ForeignKey(
-                        name: "FK_ExcludedItem_XpSettings_XpSettingsId",
-                        column: x => x.XpSettingsId,
-                        principalTable: "XpSettings",
-                        principalColumn: "Id");
-                });
-
             migrationBuilder.CreateTable(
                 name: "XpCurrencyReward",
                 columns: table => new
@@ -1863,11 +1845,6 @@ namespace EllieBot.Migrations.Sqlite
                 table: "DiscordUser",
                 column: "Username");
 
-            migrationBuilder.CreateIndex(
-                name: "IX_ExcludedItem_XpSettingsId",
-                table: "ExcludedItem",
-                column: "XpSettingsId");
-
             migrationBuilder.CreateIndex(
                 name: "IX_FeedSub_GuildId_Url",
                 table: "FeedSub",
@@ -2370,9 +2347,6 @@ namespace EllieBot.Migrations.Sqlite
             migrationBuilder.DropTable(
                 name: "DiscordPermOverrides");
 
-            migrationBuilder.DropTable(
-                name: "ExcludedItem");
-
             migrationBuilder.DropTable(
                 name: "Expressions");
 
diff --git a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
index 1b4a98c..6f16565 100644
--- a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
@@ -654,31 +654,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.ToTable("DiscordUser");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.Property<int>("Id")
-                    .ValueGeneratedOnAdd()
-                    .HasColumnType("INTEGER");
-
-                b.Property<DateTime?>("DateAdded")
-                    .HasColumnType("TEXT");
-
-                b.Property<ulong>("ItemId")
-                    .HasColumnType("INTEGER");
-
-                b.Property<int>("ItemType")
-                    .HasColumnType("INTEGER");
-
-                b.Property<int?>("XpSettingsId")
-                    .HasColumnType("INTEGER");
-
-                b.HasKey("Id");
-
-                b.HasIndex("XpSettingsId");
-
-                b.ToTable("ExcludedItem");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
             {
                 b.Property<int>("Id")
@@ -2496,9 +2471,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<bool>("ServerExcluded")
-                    .HasColumnType("INTEGER");
-
                 b.HasKey("Id");
 
                 b.HasIndex("GuildId")
@@ -2603,32 +2575,43 @@ namespace EllieBot.Migrations.Sqlite
                 b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
+                    .HasColumnType("INTEGER");
+
+                b.Property<long>("XpAmount")
                     .HasColumnType("INTEGER");
 
                 b.HasKey("Id");
 
-                b.HasAlternateKey("GuildId", "ChannelId");
+                b.HasAlternateKey("GuildId", "ChannelId", "RateType");
 
                 b.ToTable("ChannelXpConfig");
             });
 
             modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
             {
-                b.Property<ulong>("GuildId")
+                b.Property<int>("Id")
                     .ValueGeneratedOnAdd()
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("Cooldown")
+                b.Property<float>("Cooldown")
+                    .HasColumnType("REAL");
+
+                b.Property<ulong>("GuildId")
                     .HasColumnType("INTEGER");
 
-                b.Property<int>("XpAmount")
+                b.Property<int>("RateType")
+                    .HasColumnType("INTEGER");
+
+                b.Property<long>("XpAmount")
                     .HasColumnType("INTEGER");
 
                 b.Property<string>("XpTemplateUrl")
                     .HasColumnType("TEXT");
 
-                b.HasKey("GuildId");
+                b.HasKey("Id");
+
+                b.HasAlternateKey("GuildId", "RateType");
 
                 b.ToTable("GuildXpConfig");
             });
@@ -2800,13 +2783,6 @@ namespace EllieBot.Migrations.Sqlite
                 b.Navigation("Club");
             });
 
-            modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
-            {
-                b.HasOne("EllieBot.Db.Models.XpSettings", null)
-                    .WithMany("ExclusionList")
-                    .HasForeignKey("XpSettingsId");
-            });
-
             modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b =>
             {
                 b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null)
@@ -3071,8 +3047,6 @@ namespace EllieBot.Migrations.Sqlite
             {
                 b.Navigation("CurrencyRewards");
 
-                b.Navigation("ExclusionList");
-
                 b.Navigation("RoleRewards");
             });
 #pragma warning restore 612, 618
diff --git a/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs b/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs
index 4ecffe7..7ba9b23 100644
--- a/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs
+++ b/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs
@@ -11,4 +11,5 @@ public sealed class UserXpBatch
     public ulong GuildId { get; set; }
     public string Username { get; set; } = string.Empty;
     public string AvatarId { get; set; } = string.Empty;
+    public long XpToGain { get; set; } = 0;
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs
new file mode 100644
index 0000000..acf7fbb
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs
@@ -0,0 +1,172 @@
+using System.Runtime.CompilerServices;
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Common.ModuleBehaviors;
+using EllieBot.Modules.Xp.Services;
+
+namespace EllieBot.Modules.Xp;
+
+using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates);
+
+public class GuildConfigXpService(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();
+
+    public async Task OnReadyAsync()
+    {
+        await using var uow = db.GetDbContext();
+        _guildRates = await uow.GetTable<GuildXpConfig>()
+            .AsNoTracking()
+            .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
+            .ToListAsyncLinqToDB()
+            .Fmap(list =>
+                list
+                    .ToDictionary(
+                        x => (x.RateType, x.GuildId),
+                        x => new XpRate(x.RateType, x.XpAmount, x.Cooldown))
+                    .ToConcurrent());
+
+        _channelRates = await uow.GetTable<ChannelXpConfig>()
+            .AsNoTracking()
+            .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
+            .ToListAsyncLinqToDB()
+            .Fmap(x =>
+                x.GroupBy(x => x.GuildId)
+                    .ToDictionary(
+                        x => x.Key,
+                        x => x.ToDictionary(
+                                y => (y.RateType, y.ChannelId),
+                                y => new XpRate(y.RateType, y.XpAmount, y.Cooldown))
+                            .ToConcurrent())
+                    .ToConcurrent());
+    }
+
+    public async Task<GuildXpRates> GetGuildXpRatesAsync(ulong guildId)
+    {
+        await using var uow = db.GetDbContext();
+        var guildConfig = await uow.GetTable<GuildXpConfig>()
+            .AsNoTracking()
+            .Where(x => x.GuildId == guildId)
+            .ToListAsyncLinqToDB();
+
+        var channelRates = await uow.GetTable<ChannelXpConfig>()
+            .AsNoTracking()
+            .Where(x => x.GuildId == guildId)
+            .ToListAsyncLinqToDB();
+
+        return (guildConfig, channelRates);
+    }
+
+    public async Task SetGuildXpRateAsync(ulong guildId, XpRateType type, long amount, float cooldown)
+    {
+        AmountAndCooldownChecks(amount, cooldown);
+
+        if (type == XpRateType.Voice)
+            cooldown = 1.0f;
+
+        await using var uow = db.GetDbContext();
+        await uow.GetTable<GuildXpConfig>()
+            .InsertOrUpdateAsync(() => new()
+            {
+                GuildId = guildId,
+                RateType = type,
+                XpAmount = amount,
+                Cooldown = cooldown,
+            },
+                (_) => new()
+                {
+                    Cooldown = cooldown,
+                    XpAmount = amount,
+                },
+                () => new()
+                {
+                    GuildId = guildId,
+                    RateType = type,
+                });
+    }
+
+    public async Task SetChannelXpRateAsync(ulong guildId,
+        XpRateType type,
+        ulong channelId,
+        long amount,
+        float cooldown)
+    {
+        AmountAndCooldownChecks(amount, cooldown);
+
+        if (type == XpRateType.Voice)
+            cooldown = 1.0f;
+
+        await using var uow = db.GetDbContext();
+        await uow.GetTable<ChannelXpConfig>()
+            .InsertOrUpdateAsync(() => new()
+            {
+                GuildId = guildId,
+                ChannelId = channelId,
+                XpAmount = amount,
+                Cooldown = cooldown,
+                RateType = type
+            },
+                (_) => new()
+                {
+                    Cooldown = cooldown,
+                    XpAmount = amount,
+                },
+                () => new()
+                {
+                    GuildId = guildId,
+                    ChannelId = channelId,
+                    RateType = type,
+                });
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private void AmountAndCooldownChecks(long amount, float cooldown)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegative(amount, nameof(amount));
+        ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, 1000, nameof(amount));
+
+        ArgumentOutOfRangeException.ThrowIfNegative(cooldown, nameof(cooldown));
+        ArgumentOutOfRangeException.ThrowIfGreaterThan(cooldown, 1440, nameof(cooldown));
+    }
+
+    public async Task<bool> ResetGuildXpRateAsync(ulong guildId)
+    {
+        await using var uow = db.GetDbContext();
+        var deleted = await uow.GetTable<GuildXpConfig>()
+            .Where(x => x.GuildId == guildId)
+            .DeleteAsync();
+        return deleted > 0;
+    }
+
+    public async Task<bool> ResetChannelXpRateAsync(ulong guildId, ulong channelId)
+    {
+        await using var uow = db.GetDbContext();
+        var deleted = await uow.GetTable<ChannelXpConfig>()
+            .Where(x => x.GuildId == guildId && x.ChannelId == channelId)
+            .DeleteAsync();
+        return deleted > 0;
+    }
+
+    public XpRate GetXpRate(XpRateType type, ulong guildId, ulong channelId)
+    {
+        if (_channelRates.TryGetValue(guildId, out var guildChannelRates))
+        {
+            if (guildChannelRates.TryGetValue((type, channelId), out var rate))
+                return rate;
+        }
+
+        if (_guildRates.TryGetValue((type, guildId), out var guildRate))
+            return guildRate;
+
+        var conf = xcs.Data;
+
+        return type switch
+        {
+            XpRateType.Image => new XpRate(XpRateType.Image, conf.TextXpFromImage, conf.TextXpCooldown / 60.0f),
+            XpRateType.Voice => new XpRate(XpRateType.Voice, conf.VoiceXpPerMinute, 1.0f),
+            _ => new XpRate(XpRateType.Text, conf.TextXpPerMessage, conf.TextXpCooldown / 60.0f),
+        };
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRate.cs b/src/EllieBot/Modules/Xp/XpRate/XpRate.cs
new file mode 100644
index 0000000..2b283c1
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/XpRate.cs
@@ -0,0 +1,7 @@
+namespace EllieBot.Modules.Xp;
+
+public readonly record struct XpRate(XpRateType Type, long Amount, float Cooldown)
+{
+    public bool IsExcluded()
+        => Amount == 0 || Cooldown == 0;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
index c60e15e..4cb471a 100644
--- a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
+++ b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
@@ -1,10 +1,3 @@
-using LinqToDB;
-using LinqToDB.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using EllieBot.Common.ModuleBehaviors;
-using System.ComponentModel.DataAnnotations;
-
 namespace EllieBot.Modules.Xp;
 
 public partial class Xp
@@ -17,45 +10,49 @@ public partial class Xp
         public async Task XpRate()
         {
             var rates = await _service.GetGuildXpRatesAsync(ctx.Guild.Id);
-            if (rates.GuildConfig is null && !rates.ChannelRates.Any())
+            if (!rates.GuildRates.Any() && !rates.ChannelRates.Any())
             {
                 await Response().Pending(strs.xp_rate_none).SendAsync();
                 return;
             }
 
-            var eb = CreateEmbed()
-                     .WithOkColor();
-            if (rates.GuildConfig is not null)
-            {
-                eb.AddField(GetText(strs.xp_rate_server),
-                    strs.xp_rate_amount_cooldown(
-                        rates.GuildConfig.XpAmount,
-                        rates.GuildConfig.Cooldown));
-            }
+            await Response()
+                .Paginated()
+                .Items(rates.ChannelRates.GroupBy(x => x.ChannelId).ToList())
+                .PageSize(5)
+                .Page((items, _) =>
+                {
+                    var eb = CreateEmbed()
+                        .WithOkColor();
 
-            if (rates.ChannelRates.Any())
-            {
-                var channelRates = rates.ChannelRates
-                                        .Select(c => $"<#{c.ChannelId}>: {GetRateString(c.XpAmount, c.Cooldown)}")
-                                        .Join('\n');
+                    if (rates.GuildRates is not { Count: <= 0 })
+                    {
+                        eb.AddField(GetText(strs.xp_rate_server),
+                            rates.GuildRates
+                                .Select(x => GetText(strs.xp_rate_str(x.RateType, x.XpAmount, x.Cooldown)))
+                                .Join('\n'));
+                    }
 
-                eb.AddField(GetText(strs.xp_rate_channels), channelRates);
-            }
+                    if (items.Any())
+                    {
+                        var channelRates = items
+                            .Select(x => $"""
+                                          <#{x.Key}>
+                                          {x.Select(c => $"- {GetText(strs.xp_rate_str(c.RateType, c.XpAmount, c.Cooldown))}").Join('\n')}
+                                          """)
+                            .Join('\n');
 
-            await Response().Embed(eb).SendAsync();
-        }
+                        eb.AddField(GetText(strs.xp_rate_channels), channelRates);
+                    }
 
-        private string GetRateString(int argXpAmount, float cd)
-        {
-            if (argXpAmount == 0 || cd == 0)
-                return GetText(strs.xp_rate_no_gain);
-
-            return GetText(strs.xp_rate_amount_cooldown(argXpAmount, Math.Round(cd, 1).ToString(Culture)));
+                    return eb;
+                })
+                .SendAsync();
         }
 
         [Cmd]
         [RequireContext(ContextType.Guild)]
-        public async Task XpRate(int amount, float minutes)
+        public async Task XpRate(XpRateType type, int amount, float minutes)
         {
             if (amount is < 0 or > 1000)
             {
@@ -69,13 +66,13 @@ public partial class Xp
                 return;
             }
 
-            await _service.SetGuildXpRateAsync(ctx.Guild.Id, amount, (int)Math.Ceiling(minutes));
+            await _service.SetGuildXpRateAsync(ctx.Guild.Id, type, amount, (int)Math.Ceiling(minutes));
             await Response().Confirm(strs.xp_rate_server_set(amount, minutes)).SendAsync();
         }
 
         [Cmd]
         [RequireContext(ContextType.Guild)]
-        public async Task XpRate(IMessageChannel channel, int amount, float minutes)
+        public async Task XpRate(IMessageChannel channel, XpRateType type, int amount, float minutes)
         {
             if (amount is < 0 or > 1000)
             {
@@ -89,10 +86,10 @@ public partial class Xp
                 return;
             }
 
-            await _service.SetChannelXpRateAsync(ctx.Guild.Id, channel.Id, amount, (int)Math.Ceiling(minutes));
+            await _service.SetChannelXpRateAsync(ctx.Guild.Id, type, channel.Id, amount, (int)Math.Ceiling(minutes));
             await Response()
-                  .Confirm(strs.xp_rate_channel_set(Format.Bold(channel.ToString()), amount, minutes))
-                  .SendAsync();
+                .Confirm(strs.xp_rate_channel_set(Format.Bold(channel.ToString()), amount, minutes))
+                .SendAsync();
         }
 
         [Cmd]
@@ -116,139 +113,4 @@ public partial class Xp
             await Response().Confirm(strs.xp_rate_channel_reset($"<#{channelId}>")).SendAsync();
         }
     }
-}
-
-public class GuildConfigXpService : IReadyExecutor, IEService
-{
-    private readonly DbService _db;
-
-    public GuildConfigXpService(DbService db)
-    {
-        _db = db;
-    }
-
-    public async Task<(GuildXpConfig? GuildConfig, List<ChannelXpConfig> ChannelRates)> GetGuildXpRatesAsync(
-        ulong guildId)
-    {
-        await using var uow = _db.GetDbContext();
-        var guildConfig =
-            await AsyncExtensions.FirstOrDefaultAsync(uow.GetTable<GuildXpConfig>(), x => x.GuildId == guildId);
-
-        var channelRates = await AsyncExtensions.ToListAsync(uow.GetTable<ChannelXpConfig>()
-                                                                .Where(x => x.GuildId == guildId));
-
-        return (guildConfig, channelRates);
-    }
-
-    public async Task SetGuildXpRateAsync(ulong guildId, int amount, int cooldown)
-    {
-        await using var uow = _db.GetDbContext();
-        await uow.GetTable<GuildXpConfig>()
-                 .InsertOrUpdateAsync(() => new()
-                 {
-                     GuildId = guildId,
-                     XpAmount = amount,
-                     Cooldown = cooldown
-                 },
-                     (_) => new()
-                     {
-                         Cooldown = cooldown,
-                         XpAmount = amount,
-                         GuildId = guildId
-                     },
-                     () => new()
-                     {
-                         GuildId = guildId
-                     });
-    }
-
-    public async Task SetChannelXpRateAsync(
-        ulong guildId,
-        ulong channelId,
-        int amount,
-        int cooldown)
-    {
-        await using var uow = _db.GetDbContext();
-        await uow.GetTable<ChannelXpConfig>()
-                 .InsertOrUpdateAsync(() => new()
-                 {
-                     GuildId = guildId,
-                     ChannelId = channelId,
-                     XpAmount = amount,
-                     Cooldown = cooldown
-                 },
-                     (_) => new()
-                     {
-                         Cooldown = cooldown,
-                         XpAmount = amount,
-                         GuildId = guildId,
-                         ChannelId = channelId
-                     },
-                     () => new()
-                     {
-                         GuildId = guildId,
-                         ChannelId = channelId
-                     });
-    }
-
-    public async Task<bool> ResetGuildXpRateAsync(ulong guildId)
-    {
-        await using var uow = _db.GetDbContext();
-        var deleted = await uow.GetTable<GuildXpConfig>()
-                               .Where(x => x.GuildId == guildId)
-                               .DeleteAsync();
-        return deleted > 0;
-    }
-
-    public async Task<bool> ResetChannelXpRateAsync(ulong guildId, ulong channelId)
-    {
-        await using var uow = _db.GetDbContext();
-        var deleted = await uow.GetTable<ChannelXpConfig>()
-                               .Where(x => x.GuildId == guildId && x.ChannelId == channelId)
-                               .DeleteAsync();
-        return deleted > 0;
-    }
-
-    public Task OnReadyAsync()
-        => Task.CompletedTask;
-}
-
-public class GuildXpConfig
-{
-    [Key]
-    public ulong GuildId { get; set; }
-
-    public int XpAmount { get; set; }
-    public int Cooldown { get; set; }
-    public string? XpTemplateUrl { get; set; }
-}
-
-public sealed class GuildXpConfigEntity : IEntityTypeConfiguration<GuildXpConfig>
-{
-    public void Configure(EntityTypeBuilder<GuildXpConfig> builder)
-    {
-    }
-}
-
-public class ChannelXpConfig
-{
-    [Key]
-    public int Id { get; set; }
-
-    public ulong GuildId { get; set; }
-    public ulong ChannelId { get; set; }
-    public int XpAmount { get; set; }
-    public float Cooldown { get; set; }
-}
-
-public sealed class ChannelXpConfigEntity : IEntityTypeConfiguration<ChannelXpConfig>
-{
-    public void Configure(EntityTypeBuilder<ChannelXpConfig> builder)
-    {
-        builder.HasAlternateKey(x => new
-        {
-            x.GuildId,
-            x.ChannelId
-        });
-    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRateType.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateType.cs
new file mode 100644
index 0000000..f435644
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/XpRateType.cs
@@ -0,0 +1,8 @@
+namespace EllieBot.Modules.Xp;
+
+public enum XpRateType
+{
+    Text,
+    Voice,
+    Image,
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs b/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs
new file mode 100644
index 0000000..f2b3f9b
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace EllieBot.Modules.Xp;
+
+public class ChannelXpConfig
+{
+    [Key]
+    public int Id { get; set; }
+
+    public XpRateType RateType { get; set; }
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public long XpAmount { get; set; }
+    public float Cooldown { get; set; }
+}
+
+public sealed class ChannelXpConfigEntity : IEntityTypeConfiguration<ChannelXpConfig>
+{
+    public void Configure(EntityTypeBuilder<ChannelXpConfig> builder)
+    {
+        builder.HasAlternateKey(x => new
+        {
+            x.GuildId,
+            x.ChannelId,
+            x.RateType,
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs b/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs
new file mode 100644
index 0000000..7bf510d
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace EllieBot.Modules.Xp;
+
+public class GuildXpConfig
+{
+    [Key]
+    public int Id { get; set; }
+
+    public ulong GuildId { get; set; }
+    public XpRateType RateType { get; set; }
+
+    public long XpAmount { get; set; }
+    public float Cooldown { get; set; }
+    public string? XpTemplateUrl { get; set; }
+}
+
+public sealed class GuildXpConfigEntity : IEntityTypeConfiguration<GuildXpConfig>
+{
+    public void Configure(EntityTypeBuilder<GuildXpConfig> builder)
+    {
+        builder.HasAlternateKey(x => new
+        {
+            x.GuildId,
+            x.RateType,
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs
index 552c3fe..a503632 100644
--- a/src/EllieBot/Modules/Xp/XpService.cs
+++ b/src/EllieBot/Modules/Xp/XpService.cs
@@ -32,11 +32,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     private readonly ICurrencyService _cs;
     private readonly IHttpClientFactory _httpFactory;
     private readonly XpConfigService _xpConfig;
-    private readonly IPubSub _pubSub;
-
-    private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedRoles = new();
-    private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels = new();
-    private readonly ConcurrentHashSet<ulong> _excludedServers;
 
     private readonly DiscordSocketClient _client;
 
@@ -45,8 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
     private readonly INotifySubscriber _notifySub;
     private readonly IMemoryCache _memCache;
-    private readonly ShardData _shardData;
     private readonly XpTemplateService _templateService;
+    private readonly GuildConfigXpService _xpRateService;
 
     private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
 
@@ -65,7 +60,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         INotifySubscriber notifySub,
         IMemoryCache memCache,
         ShardData shardData,
-        XpTemplateService templateService)
+        XpTemplateService templateService,
+        GuildConfigXpService xpRateService)
     {
         _db = db;
         _images = images;
@@ -74,47 +70,17 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         _cs = cs;
         _httpFactory = http;
         _xpConfig = xpConfig;
-        _pubSub = pubSub;
         _notifySub = notifySub;
         _memCache = memCache;
-        _shardData = shardData;
         _templateService = templateService;
-        _excludedServers = [];
-        _excludedChannels = new();
+        _xpRateService = xpRateService;
         _client = client;
         _ps = ps;
         _c = c;
-
-        if (client.ShardId == 0)
-        {
-        }
     }
 
     public async Task OnReadyAsync()
     {
-        // initialize ignored
-        await using (var ctx = _db.GetDbContext())
-        {
-            var xps = await ctx.GetTable<XpSettings>()
-                .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
-                .LoadWith(x => x.ExclusionList)
-                .ToListAsyncLinqToDB();
-
-            foreach (var xp in xps)
-            {
-                if (xp.ServerExcluded)
-                    _excludedServers.Add(xp.GuildId);
-
-                foreach (var item in xp.ExclusionList)
-                {
-                    if (item.ItemType == ExcludedItemType.Channel)
-                        _excludedChannels.GetOrAdd(xp.GuildId, static _ => []).Add(item.ItemId);
-                    else if (item.ItemType == ExcludedItemType.Role)
-                        _excludedRoles.GetOrAdd(xp.GuildId, static _ => []).Add(item.ItemId);
-                }
-            }
-        }
-
         await Task.WhenAll(UpdateTimer(), VoiceUpdateTimer(), _levelUpQueue.RunAsync());
 
         return;
@@ -156,65 +122,66 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     /// <summary>
     /// The current batch of users that will gain xp
     /// </summary>
-    private readonly ConcurrentHashSet<IGuildUser> _usersBatch = [];
+    private readonly ConcurrentHashSet<XpQueueEntry> _usersBatch = [];
 
     /// <summary>
     /// The current batch of users that will gain voice xp
     /// </summary>
-    private readonly ConcurrentHashSet<IGuildUser> _voiceXpBatch = [];
+    private readonly HashSet<IGuildUser> _voiceXpBatch = [];
 
     private async Task UpdateVoiceXp()
     {
-        var xpAmount = _xpConfig.Data.VoiceXpPerMinute;
-
-        if (xpAmount <= 0)
-            return;
-
-        var oldBatch = _voiceXpBatch.ToArray();
+        var oldBatch = _voiceXpBatch.ToHashSet();
         _voiceXpBatch.Clear();
-        var validUsers = new HashSet<IGuildUser>();
+        var validUsers = new List<XpQueueEntry>(oldBatch.Count);
 
         var guilds = _client.Guilds;
 
         foreach (var g in guilds)
         {
-            if (IsServerExcluded(g.Id))
-                continue;
-
             foreach (var vc in g.VoiceChannels)
             {
                 if (!IsVoiceChannelActive(vc))
                     continue;
 
-                if (IsChannelExcluded(vc))
+                var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
+
+                if (rate.IsExcluded())
                     continue;
 
                 foreach (var u in vc.ConnectedUsers)
                 {
-                    if (IsServerOrRoleExcluded(u) || !UserParticipatingInVoiceChannel(u))
+                    if (!UserParticipatingInVoiceChannel(u))
                         continue;
 
                     if (oldBatch.Contains(u))
-                        validUsers.Add(u);
+                    {
+                        validUsers.Add(new(u, rate.Amount));
+                    }
 
                     _voiceXpBatch.Add(u);
                 }
             }
         }
 
-        await UpdateXpInternalAsync(validUsers.DistinctBy(x => x.Id).ToArray(), xpAmount);
+        await UpdateXpInternalAsync(validUsers.ToArray());
     }
 
     private async Task UpdateXp()
     {
-        var xpAmount = _xpConfig.Data.TextXpPerMessage;
+        // might want to lock this, but it's not a big deal
+
+        // or do something like this
+        // foreach (var item in currentBatch)
+        //     _usersBatch.TryRemove(item);
+
         var currentBatch = _usersBatch.ToArray();
         _usersBatch.Clear();
 
-        await UpdateXpInternalAsync(currentBatch, xpAmount);
+        await UpdateXpInternalAsync(currentBatch);
     }
 
-    private async Task UpdateXpInternalAsync(IGuildUser[] currentBatch, int xpAmount)
+    private async Task UpdateXpInternalAsync(XpQueueEntry[] currentBatch)
     {
         if (currentBatch.Length == 0)
             return;
@@ -227,16 +194,17 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
         await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch()
         {
-            GuildId = x.GuildId,
-            UserId = x.Id,
-            Username = x.Username,
-            AvatarId = x.DisplayAvatarId
+            GuildId = x.User.GuildId,
+            UserId = x.User.Id,
+            Username = x.User.Username,
+            AvatarId = x.User.DisplayAvatarId,
+            XpToGain = x.Xp
         }));
 
         await lctx.ExecuteAsync(
             $"""
              INSERT INTO UserXpStats (GuildId, UserId, Xp)
-             SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", {xpAmount}
+             SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", "XpToGain"
              FROM {tempTableName}
              WHERE TRUE
              ON CONFLICT (GuildId, UserId) DO UPDATE 
@@ -251,9 +219,13 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                 (batch, stats) => stats)
             .ToListAsyncLinqToDB();
 
+        var userToXp = currentBatch.ToDictionary(x => x.User.Id, x => x.Xp);
         foreach (var u in updated)
         {
-            var oldStats = new LevelStats(u.Xp - xpAmount);
+            if (!userToXp.TryGetValue(u.UserId, out var xpGained))
+                continue;
+
+            var oldStats = new LevelStats(u.Xp - xpGained);
             var newStats = new LevelStats(u.Xp);
 
             Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}",
@@ -531,8 +503,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         {
             if (UserParticipatingInVoiceChannel(user))
             {
-                count++;
-                if (count >= 2)
+                if (++count >= 2)
                     return true;
             }
         }
@@ -543,27 +514,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     private static bool UserParticipatingInVoiceChannel(SocketGuildUser user)
         => !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
 
-    private bool IsServerOrRoleExcluded(SocketGuildUser user)
-    {
-        if (_excludedServers.Contains(user.Guild.Id))
-            return true;
-
-        if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && user.Roles.Any(x => roles.Contains(x.Id)))
-            return true;
-
-        return false;
-    }
-
-    private bool IsChannelExcluded(IGuildChannel channel)
-    {
-        if (_excludedChannels.TryGetValue(channel.Guild.Id, out var chans)
-            && (chans.Contains(channel.Id)
-                || (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
-            return true;
-
-        return false;
-    }
-
     public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg)
     {
         if (arg.Author is not SocketGuildUser user || user.IsBot)
@@ -574,27 +524,36 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
         _ = Task.Run(async () =>
         {
-            if (IsChannelExcluded(gc))
+            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);
+
+            XpRate rate;
+            if (isImage)
+            {
+                var imageRate = _xpRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
+                if (imageRate.IsExcluded())
+                    return;
+
+                rate = imageRate;
+            }
+            else if (isText)
+            {
+                if (textRate.IsExcluded())
+                    return;
+
+                rate = textRate;
+            }
+            else
+            {
+                return;
+            }
+
+            if (!await TryAddUserGainedXpAsync(user.Id, rate.Cooldown))
                 return;
 
-            if (IsServerOrRoleExcluded(user))
-                return;
-
-            var xpConf = _xpConfig.Data;
-            var xp = 0;
-            if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128))
-                xp = xpConf.TextXpFromImage;
-
-            if (arg.Content.Contains(' ') || arg.Content.Length >= 5)
-                xp = Math.Max(xp, xpConf.TextXpPerMessage);
-
-            if (xp <= 0)
-                return;
-
-            if (!await TryAddUserGainedXpAsync(user.Id, xpConf.TextXpCooldown))
-                return;
-
-            _usersBatch.Add(user);
+            _usersBatch.Add(new(user, rate.Amount));
         });
 
         return Task.CompletedTask;
@@ -621,28 +580,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         await uow.SaveChangesAsync();
     }
 
-    public bool IsServerExcluded(ulong id)
-        => _excludedServers.Contains(id);
-
-    public IEnumerable<ulong> GetExcludedRoles(ulong id)
+    private Task<bool> TryAddUserGainedXpAsync(ulong userId, float cdInMinutes)
     {
-        if (_excludedRoles.TryGetValue(id, out var val))
-            return val.ToArray();
-
-        return [];
-    }
-
-    public IEnumerable<ulong> GetExcludedChannels(ulong id)
-    {
-        if (_excludedChannels.TryGetValue(id, out var val))
-            return val.ToArray();
-
-        return [];
-    }
-
-    private Task<bool> TryAddUserGainedXpAsync(ulong userId, int cdInSeconds)
-    {
-        if (cdInSeconds <= 0)
+        if (cdInMinutes <= float.Epsilon)
             return Task.FromResult(true);
 
         if (_memCache.TryGetValue(userId, out _))
@@ -651,7 +591,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         using var entry = _memCache.CreateEntry(userId);
         entry.Value = true;
 
-        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cdInSeconds);
+        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cdInMinutes);
 
         return Task.FromResult(true);
     }
@@ -674,81 +614,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
             guildRank);
     }
 
-    public async Task<bool> ToggleExcludeServerAsync(ulong id)
-    {
-        await using var uow = _db.GetDbContext();
-        var xpSetting = await uow.XpSettingsFor(id);
-        if (_excludedServers.Add(id))
-        {
-            xpSetting.ServerExcluded = true;
-            await uow.SaveChangesAsync();
-            return true;
-        }
-
-        _excludedServers.TryRemove(id);
-        xpSetting.ServerExcluded = false;
-        await uow.SaveChangesAsync();
-        return false;
-    }
-
-    public async Task<bool> ToggleExcludeRoleAsync(ulong guildId, ulong rId)
-    {
-        var roles = _excludedRoles.GetOrAdd(guildId, _ => []);
-        await using var uow = _db.GetDbContext();
-        var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
-        var excludeObj = new ExcludedItem
-        {
-            ItemId = rId,
-            ItemType = ExcludedItemType.Role
-        };
-
-        if (roles.Add(rId))
-        {
-            if (xpSetting.ExclusionList.Add(excludeObj))
-                await uow.SaveChangesAsync();
-
-            return true;
-        }
-
-        roles.TryRemove(rId);
-
-        var toDelete = xpSetting.ExclusionList.FirstOrDefault(x => x.Equals(excludeObj));
-        if (toDelete is not null)
-        {
-            uow.Remove(toDelete);
-            await uow.SaveChangesAsync();
-        }
-
-        return false;
-    }
-
-    public async Task<bool> ToggleExcludeChannelAsync(ulong guildId, ulong chId)
-    {
-        var channels = _excludedChannels.GetOrAdd(guildId, _ => []);
-        await using var uow = _db.GetDbContext();
-        var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
-        var excludeObj = new ExcludedItem
-        {
-            ItemId = chId,
-            ItemType = ExcludedItemType.Channel
-        };
-
-        if (channels.Add(chId))
-        {
-            if (xpSetting.ExclusionList.Add(excludeObj))
-                await uow.SaveChangesAsync();
-
-            return true;
-        }
-
-        channels.TryRemove(chId);
-
-        if (xpSetting.ExclusionList.Remove(excludeObj))
-            await uow.SaveChangesAsync();
-
-        return false;
-    }
-
     public async Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(IGuildUser user)
     {
         var stats = await GetUserStatsAsync(user);
@@ -1224,8 +1089,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
             if (item is null || item.Price < 0)
                 return BuyResult.UnknownItem;
 
-            if (item.Price > 0 &&
-                !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
+            if (item.Price > 0 && !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
                 return BuyResult.InsufficientFunds;
 
 
@@ -1438,4 +1302,13 @@ public sealed class XpTemplateService : IEService, IReadyExecutor
 
     public XpTemplate GetTemplate()
         => _template;
+}
+
+public readonly record struct XpQueueEntry(IGuildUser User, long Xp)
+{
+    public bool Equals(XpQueueEntry? other)
+        => other?.User == User;
+
+    public override int GetHashCode()
+        => User.GetHashCode();
 }
\ No newline at end of file
diff --git a/src/EllieBot/Services/GrpcApi/XpSvc.cs b/src/EllieBot/Services/GrpcApi/XpSvc.cs
index 3cf9167..674dc48 100644
--- a/src/EllieBot/Services/GrpcApi/XpSvc.cs
+++ b/src/EllieBot/Services/GrpcApi/XpSvc.cs
@@ -35,9 +35,9 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
         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 excludedChannels = new List<ulong>();
+        var excludedRoles = new List<ulong>();
+        var isServerExcluded = false;
 
         var reply = new GetXpSettingsReply();
 
@@ -82,59 +82,6 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
         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 = await _xp.ToggleExcludeRoleAsync(request.GuildId, request.Id);
-        }
-        else if (request.Type == "Channel")
-        {
-            if (guild.GetTextChannel(request.Id) is null)
-                return new()
-                {
-                    Success = false
-                };
-
-            success = await _xp.ToggleExcludeChannelAsync(request.GuildId, request.Id);
-        }
-
-        return new()
-        {
-            Success = success
-        };
-    }
-
-    public override async Task<DeleteExclusionReply> DeleteExclusion(
-        DeleteExclusionRequest request,
-        ServerCallContext context)
-    {
-        var success = false;
-        if (request.Type == "Role")
-            success = await _xp.ToggleExcludeRoleAsync(request.GuildId, request.Id);
-        else
-            success = await _xp.ToggleExcludeChannelAsync(request.GuildId, request.Id);
-
-        return new DeleteExclusionReply
-        {
-            Success = success
-        };
-    }
-
     public override async Task<AddRewardReply> AddReward(AddRewardRequest request, ServerCallContext context)
     {
         await Task.Yield();
@@ -267,15 +214,4 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
 
         return reply;
     }
-
-    public override async Task<SetServerExclusionReply> SetServerExclusion(
-        SetServerExclusionRequest request,
-        ServerCallContext context)
-    {
-        var newValue = await _xp.ToggleExcludeServerAsync(request.GuildId);
-        return new()
-        {
-            Success = newValue
-        };
-    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/_common/Interaction/EllieInteraction.cs b/src/EllieBot/_common/Interaction/EllieInteraction.cs
index 0206599..a3f67db 100644
--- a/src/EllieBot/_common/Interaction/EllieInteraction.cs
+++ b/src/EllieBot/_common/Interaction/EllieInteraction.cs
@@ -40,7 +40,7 @@ public abstract class EllieInteractionBase
         message = msg;
 
         Client.InteractionCreated += OnInteraction;
-        await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
+        await Task.WhenAny(Task.Delay(600_000), _interactionCompletedSource.Task);
         Client.InteractionCreated -= OnInteraction;
 
         if (_clearAfter)
@@ -130,7 +130,7 @@ public sealed class EllieModalSubmitHandler
         message = msg;
 
         Client.ModalSubmitted += OnInteraction;
-        await Task.WhenAny(Task.Delay(300_000), _interactionCompletedSource.Task);
+        await Task.WhenAny(Task.Delay(600_000), _interactionCompletedSource.Task);
         Client.ModalSubmitted -= OnInteraction;
 
         await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
diff --git a/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs b/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs
index 92d871a..915a3ed 100644
--- a/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs
+++ b/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs
@@ -159,7 +159,7 @@ public partial class ResponseBuilder
             
             await Task.WhenAll(left.RunAsync(msg), extra?.RunAsync(msg) ?? Task.CompletedTask, right.RunAsync(msg));
 
-            await Task.Delay(30_000);
+            await Task.Delay(600_000);
             
             await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
         }
diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml
index cb691ca..e366798 100644
--- a/src/EllieBot/strings/commands/commands.en-US.yml
+++ b/src/EllieBot/strings/commands/commands.en-US.yml
@@ -4935,20 +4935,25 @@ fishspot:
 xprate:
   desc: |-
     Sets the xp rate for the server or the specified channel.
-    First specify the amount, and then the cooldown in minutes.
+    First specify the type, amount, and then the cooldown in minutes.
     Provide no parameters to see the current rates.
+    Cooldown has no effect on voice xp, as any amount is gained per minute.
   ex:
     - ''
-    - '3 5'
-    - '#channel 50 1'
+    - 'text 3 5'
+    - '#channel voice 50 1'
   params:
     - { }
-    - amount:
+    - type:
+        desc: "The type of rate to set. One of: text, voice or image."
+      amount:
         desc: "The amount of xp to give per message."
       minutes:
         desc: "The cooldown in minutes. Allows decimal values."
     - channel:
         desc: "The channel to set the rate for."
+      type:
+        desc: "The type of rate to set. One of: text, voice or image."
       amount:
         desc: "The amount of xp to give per message."
       minutes:
diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json
index e5dadaf..886c8eb 100644
--- a/src/EllieBot/strings/responses/responses.en-US.json
+++ b/src/EllieBot/strings/responses/responses.en-US.json
@@ -1170,12 +1170,12 @@
   "fish_list_title": "Fishing",
   "xp_rate_none": "No xp rate overrides on this server.",
   "xp_rate_amount_invalid": "Amount must be between 0 and 1000.",
-  "xp_rate_cooldown_invalid": "Cooldown must be between 0 and 1440 minutes.",
-  "xp_rate_server": "Server xp Rate",
-  "xp_rate_amount_cooldown": "{0} xp per every {1} minutes",
-  "xp_rate_channels": "Channel XP Rates",
-  "xp_rate_server_set": "Server xp rate set to {0} xp per every {1} minutes.",
-  "xp_rate_channel_set": "Channel {0} xp rate set to {1} xp per every {2} minutes.",
+  "xp_rate_cooldown_invalid": "Cooldown must be between 0 and 1440.",
+  "xp_rate_server": "Server xp rates",
+  "xp_rate_str": "`{0}:` {1} xp every {2} min.",
+  "xp_rate_channels": "Channel xp rates",
+  "xp_rate_server_set": "Server xp rate set to **{0}** xp per every **{1}** min.",
+  "xp_rate_channel_set": "Channel **{0}** xp rate set to **{1}** xp per every **{2}** min.",
   "xp_rate_server_reset": "Server xp rate has been reset to global defaults.",
   "xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
   "xp_rate_no_gain": "No xp gain"