diff --git a/src/EllieBot/.editorconfig b/src/EllieBot/.editorconfig
index 304861d..9814958 100644
--- a/src/EllieBot/.editorconfig
+++ b/src/EllieBot/.editorconfig
@@ -356,3 +356,5 @@ resharper_arrange_redundant_parentheses_highlighting = hint
 
 # IDE0011: Add braces
 dotnet_diagnostic.IDE0011.severity = warning
+
+resharper_arrange_type_member_modifiers_highlighting = hint
\ No newline at end of file
diff --git a/src/EllieBot/Db/EllieContext.cs b/src/EllieBot/Db/EllieContext.cs
index a009eaa..6b86daf 100644
--- a/src/EllieBot/Db/EllieContext.cs
+++ b/src/EllieBot/Db/EllieContext.cs
@@ -81,7 +81,7 @@ public abstract class EllieContext : DbContext
             e.HasAlternateKey(x => new
             {
                 x.GuildId,
-                x.Event
+                Event = x.Type
             });
         });
 
diff --git a/src/EllieBot/Db/Models/Notify.cs b/src/EllieBot/Db/Models/Notify.cs
index 77c2de0..90d30d3 100644
--- a/src/EllieBot/Db/Models/Notify.cs
+++ b/src/EllieBot/Db/Models/Notify.cs
@@ -6,15 +6,17 @@ public class Notify
 {
     [Key]
     public int Id { get; set; }
+
     public ulong GuildId { get; set; }
     public ulong ChannelId { get; set; }
-    public NotifyEvent Event { get; set; }
+    public NotifyType Type { get; set; }
 
     [MaxLength(10_000)]
     public string Message { get; set; } = string.Empty;
 }
 
-public enum NotifyEvent
+public enum NotifyType
 {
-    UserLevelUp
+    LevelUp = 0,
+    Protection = 1, Prot = 1,
 }
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs b/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs
deleted file mode 100644
index a292026..0000000
--- a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace EllieBot.Migrations.PostgreSql
-{
-    /// <inheritdoc />
-    public partial class awardedxpandnotifyremoved : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "ix_userxpstats_awardedxp",
-                table: "userxpstats");
-
-            migrationBuilder.DropColumn(
-                name: "awardedxp",
-                table: "userxpstats");
-
-            migrationBuilder.DropColumn(
-                name: "notifyonlevelup",
-                table: "userxpstats");
-
-            migrationBuilder.DropColumn(
-                name: "dateadded",
-                table: "sargroup");
-
-            migrationBuilder.CreateTable(
-                name: "notify",
-                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),
-                    channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    @event = table.Column<int>(name: "event", type: "integer", nullable: false),
-                    message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("pk_notify", x => x.id);
-                    table.UniqueConstraint("ak_notify_guildid_event", x => new { x.guildid, x.@event });
-                });
-
-            migrationBuilder.CreateTable(
-                name: "temprole",
-                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),
-                    remove = table.Column<bool>(type: "boolean", nullable: false),
-                    roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    expiresat = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("pk_temprole", x => x.id);
-                    table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid });
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "ix_temprole_expiresat",
-                table: "temprole",
-                column: "expiresat");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "notify");
-
-            migrationBuilder.DropTable(
-                name: "temprole");
-
-            migrationBuilder.AddColumn<long>(
-                name: "awardedxp",
-                table: "userxpstats",
-                type: "bigint",
-                nullable: false,
-                defaultValue: 0L);
-
-            migrationBuilder.AddColumn<int>(
-                name: "notifyonlevelup",
-                table: "userxpstats",
-                type: "integer",
-                nullable: false,
-                defaultValue: 0);
-
-            migrationBuilder.AddColumn<DateTime>(
-                name: "dateadded",
-                table: "sargroup",
-                type: "timestamp without time zone",
-                nullable: true);
-
-            migrationBuilder.CreateIndex(
-                name: "ix_userxpstats_awardedxp",
-                table: "userxpstats",
-                column: "awardedxp");
-        }
-    }
-}
diff --git a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs
similarity index 99%
rename from src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs
rename to src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs
index 1a47e18..b56d51a 100644
--- a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace EllieBot.Migrations.PostgreSql
 {
     [DbContext(typeof(PostgreSqlContext))]
-    [Migration("20241208035947_awarded-xp-and-notify-removed")]
-    partial class awardedxpandnotifyremoved
+    [Migration("20241208053644_awardedxp-temprole-notify")]
+    partial class awardedxptemprolenotify
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -1833,10 +1833,6 @@ namespace EllieBot.Migrations.PostgreSql
                         .HasColumnType("numeric(20,0)")
                         .HasColumnName("channelid");
 
-                    b.Property<int>("Event")
-                        .HasColumnType("integer")
-                        .HasColumnName("event");
-
                     b.Property<decimal>("GuildId")
                         .HasColumnType("numeric(20,0)")
                         .HasColumnName("guildid");
@@ -1847,11 +1843,15 @@ namespace EllieBot.Migrations.PostgreSql
                         .HasColumnType("character varying(10000)")
                         .HasColumnName("message");
 
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
                     b.HasKey("Id")
                         .HasName("pk_notify");
 
-                    b.HasAlternateKey("GuildId", "Event")
-                        .HasName("ak_notify_guildid_event");
+                    b.HasAlternateKey("GuildId", "Type")
+                        .HasName("ak_notify_guildid_type");
 
                     b.ToTable("notify", (string)null);
                 });
diff --git a/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs
new file mode 100644
index 0000000..a76cc4a
--- /dev/null
+++ b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs
@@ -0,0 +1,46 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace EllieBot.Migrations.PostgreSql
+{
+    /// <inheritdoc />
+    public partial class awardedxptemprolenotify : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropUniqueConstraint(
+                name: "ak_notify_guildid_event",
+                table: "notify");
+
+            migrationBuilder.RenameColumn(
+                name: "event",
+                table: "notify",
+                newName: "type");
+
+            migrationBuilder.AddUniqueConstraint(
+                name: "ak_notify_guildid_type",
+                table: "notify",
+                columns: new[] { "guildid", "type" });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropUniqueConstraint(
+                name: "ak_notify_guildid_type",
+                table: "notify");
+
+            migrationBuilder.RenameColumn(
+                name: "type",
+                table: "notify",
+                newName: "event");
+
+            migrationBuilder.AddUniqueConstraint(
+                name: "ak_notify_guildid_event",
+                table: "notify",
+                columns: new[] { "guildid", "event" });
+        }
+    }
+}
diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
index bb98853..e691bbc 100644
--- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
@@ -1830,10 +1830,6 @@ namespace EllieBot.Migrations.PostgreSql
                         .HasColumnType("numeric(20,0)")
                         .HasColumnName("channelid");
 
-                    b.Property<int>("Event")
-                        .HasColumnType("integer")
-                        .HasColumnName("event");
-
                     b.Property<decimal>("GuildId")
                         .HasColumnType("numeric(20,0)")
                         .HasColumnName("guildid");
@@ -1844,11 +1840,15 @@ namespace EllieBot.Migrations.PostgreSql
                         .HasColumnType("character varying(10000)")
                         .HasColumnName("message");
 
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
                     b.HasKey("Id")
                         .HasName("pk_notify");
 
-                    b.HasAlternateKey("GuildId", "Event")
-                        .HasName("ak_notify_guildid_event");
+                    b.HasAlternateKey("GuildId", "Type")
+                        .HasName("ak_notify_guildid_type");
 
                     b.ToTable("notify", (string)null);
                 });
diff --git a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs b/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs
deleted file mode 100644
index af5984c..0000000
--- a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace EllieBot.Migrations
-{
-    /// <inheritdoc />
-    public partial class awardedxpandnotifyremoved : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_UserXpStats_AwardedXp",
-                table: "UserXpStats");
-
-            migrationBuilder.DropColumn(
-                name: "AwardedXp",
-                table: "UserXpStats");
-
-            migrationBuilder.DropColumn(
-                name: "NotifyOnLevelUp",
-                table: "UserXpStats");
-
-            migrationBuilder.DropColumn(
-                name: "DateAdded",
-                table: "SarGroup");
-
-            migrationBuilder.CreateTable(
-                name: "Notify",
-                columns: table => new
-                {
-                    Id = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    Event = table.Column<int>(type: "INTEGER", nullable: false),
-                    Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Notify", x => x.Id);
-                    table.UniqueConstraint("AK_Notify_GuildId_Event", x => new { x.GuildId, x.Event });
-                });
-
-            migrationBuilder.CreateTable(
-                name: "TempRole",
-                columns: table => new
-                {
-                    Id = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    Remove = table.Column<bool>(type: "INTEGER", nullable: false),
-                    RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_TempRole", x => x.Id);
-                    table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId });
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_TempRole_ExpiresAt",
-                table: "TempRole",
-                column: "ExpiresAt");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Notify");
-
-            migrationBuilder.DropTable(
-                name: "TempRole");
-
-            migrationBuilder.AddColumn<long>(
-                name: "AwardedXp",
-                table: "UserXpStats",
-                type: "INTEGER",
-                nullable: false,
-                defaultValue: 0L);
-
-            migrationBuilder.AddColumn<int>(
-                name: "NotifyOnLevelUp",
-                table: "UserXpStats",
-                type: "INTEGER",
-                nullable: false,
-                defaultValue: 0);
-
-            migrationBuilder.AddColumn<DateTime>(
-                name: "DateAdded",
-                table: "SarGroup",
-                type: "TEXT",
-                nullable: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_UserXpStats_AwardedXp",
-                table: "UserXpStats",
-                column: "AwardedXp");
-        }
-    }
-}
diff --git a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs
similarity index 99%
rename from src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs
rename to src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs
index 5424538..c9b5e7e 100644
--- a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs
+++ b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs
@@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace EllieBot.Migrations
 {
     [DbContext(typeof(SqliteContext))]
-    [Migration("20241208035845_awarded-xp-and-notify-removed")]
-    partial class awardedxpandnotifyremoved
+    [Migration("20241208053549_awardedxp-temprole-notify")]
+    partial class awardedxptemprolenotify
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -1368,9 +1368,6 @@ namespace EllieBot.Migrations
                     b.Property<ulong>("ChannelId")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int>("Event")
-                        .HasColumnType("INTEGER");
-
                     b.Property<ulong>("GuildId")
                         .HasColumnType("INTEGER");
 
@@ -1379,9 +1376,12 @@ namespace EllieBot.Migrations
                         .HasMaxLength(10000)
                         .HasColumnType("TEXT");
 
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
                     b.HasKey("Id");
 
-                    b.HasAlternateKey("GuildId", "Event");
+                    b.HasAlternateKey("GuildId", "Type");
 
                     b.ToTable("Notify");
                 });
diff --git a/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs
new file mode 100644
index 0000000..8281303
--- /dev/null
+++ b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs
@@ -0,0 +1,46 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace EllieBot.Migrations
+{
+    /// <inheritdoc />
+    public partial class awardedxptemprolenotify : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropUniqueConstraint(
+                name: "AK_Notify_GuildId_Event",
+                table: "Notify");
+
+            migrationBuilder.RenameColumn(
+                name: "Event",
+                table: "Notify",
+                newName: "Type");
+
+            migrationBuilder.AddUniqueConstraint(
+                name: "AK_Notify_GuildId_Type",
+                table: "Notify",
+                columns: new[] { "GuildId", "Type" });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropUniqueConstraint(
+                name: "AK_Notify_GuildId_Type",
+                table: "Notify");
+
+            migrationBuilder.RenameColumn(
+                name: "Type",
+                table: "Notify",
+                newName: "Event");
+
+            migrationBuilder.AddUniqueConstraint(
+                name: "AK_Notify_GuildId_Event",
+                table: "Notify",
+                columns: new[] { "GuildId", "Event" });
+        }
+    }
+}
diff --git a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs
index 4f03454..657bd0a 100644
--- a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs
@@ -1365,9 +1365,6 @@ namespace EllieBot.Migrations
                     b.Property<ulong>("ChannelId")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int>("Event")
-                        .HasColumnType("INTEGER");
-
                     b.Property<ulong>("GuildId")
                         .HasColumnType("INTEGER");
 
@@ -1376,9 +1373,12 @@ namespace EllieBot.Migrations
                         .HasMaxLength(10000)
                         .HasColumnType("TEXT");
 
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
                     b.HasKey("Id");
 
-                    b.HasAlternateKey("GuildId", "Event");
+                    b.HasAlternateKey("GuildId", "Type");
 
                     b.ToTable("Notify");
                 });
diff --git a/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs
new file mode 100644
index 0000000..9fb1d80
--- /dev/null
+++ b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs
@@ -0,0 +1,23 @@
+using EllieBot.Db.Models;
+using System.Collections;
+
+namespace EllieBot.Modules.Administration;
+
+public interface INotifyModel
+{
+    static abstract string KeyName { get; }
+    static abstract NotifyType NotifyType { get; }
+    IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements();
+
+    public virtual bool TryGetGuildId(out ulong guildId)
+    {
+        guildId = 0;
+        return false;
+    }
+
+    public virtual bool TryGetUserId(out ulong userId)
+    {
+        userId = 0;
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs
new file mode 100644
index 0000000..f0a8731
--- /dev/null
+++ b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs
@@ -0,0 +1,7 @@
+namespace EllieBot.Modules.Administration;
+
+public interface INotifySubscriber
+{
+    Task NotifyAsync<T>(T data, bool isShardLocal = false)
+        where T : struct, INotifyModel;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs
new file mode 100644
index 0000000..3495c09
--- /dev/null
+++ b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs
@@ -0,0 +1,44 @@
+using EllieBot.Db.Models;
+
+namespace EllieBot.Modules.Administration;
+
+public record struct LevelUpNotifyModel(
+    ulong GuildId,
+    ulong ChannelId,
+    ulong UserId,
+    long Level) : INotifyModel
+{
+    public static string KeyName
+        => "notify.levelup";
+
+    public static NotifyType NotifyType
+        => NotifyType.LevelUp;
+
+    public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
+    {
+        var data = this;
+        return new Dictionary<string, Func<SocketGuild, string>>()
+        {
+            { "%event.level%", g => data.Level.ToString() },
+        };
+    }
+
+    public bool TryGetGuildId(out ulong guildId)
+    {
+        guildId = GuildId;
+        return true;
+    }
+
+    public bool TryGetUserId(out ulong userId)
+    {
+        userId = UserId;
+        return true;
+    }
+}
+
+public static class INotifyModelExtensions
+{
+    public static TypedKey<T> GetTypedKey<T>(this T model)
+        where T : struct, INotifyModel
+        => new(T.KeyName);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs
index 2b33f34..592573a 100644
--- a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs
+++ b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs
@@ -1,92 +1,24 @@
-using LinqToDB;
-using LinqToDB.EntityFrameworkCore;
-using EllieBot.Common.ModuleBehaviors;
-using EllieBot.Db.Models;
+using EllieBot.Db.Models;
 
 namespace EllieBot.Modules.Administration;
 
-public sealed class NotifyService : IReadyExecutor, IEService
-{
-    private readonly DbService _db;
-    private readonly IMessageSenderService _mss;
-    private readonly DiscordSocketClient _client;
-    private readonly IBotCreds _creds;
-
-    public NotifyService(
-        DbService db,
-        IMessageSenderService mss,
-        DiscordSocketClient client,
-        IBotCreds creds)
-    {
-        _db = db;
-        _mss = mss;
-        _client = client;
-        _creds = creds;
-    }
-
-    public async Task OnReadyAsync()
-    {
-        // .Where(x => Linq2DbExpressions.GuildOnShard(guildId,
-        // _creds.TotalShards,
-        // _client.ShardId))
-    }
-
-    public async Task EnableAsync(
-        ulong guildId,
-        ulong channelId,
-        NotifyEvent nEvent,
-        string message)
-    {
-        await using var uow = _db.GetDbContext();
-        await uow.GetTable<Notify>()
-                 .InsertOrUpdateAsync(() => new()
-                 {
-                     GuildId = guildId,
-                     ChannelId = channelId,
-                     Event = nEvent,
-                     Message = message,
-                 },
-                     (_) => new()
-                     {
-                         Message = message,
-                         ChannelId = channelId
-                     },
-                     () => new()
-                     {
-                         GuildId = guildId,
-                         Event = nEvent
-                     });
-    }
-
-    public async Task DisableAsync(ulong guildId, NotifyEvent nEvent)
-    {
-        await using var uow = _db.GetDbContext();
-        var deleted = await uow.GetTable<Notify>()
-                               .Where(x => x.GuildId == guildId && x.Event == nEvent)
-                               .DeleteAsync();
-
-        if (deleted > 0)
-            return;
-    }
-}
-
 public partial class Administration
 {
     public class NotifyCommands : EllieModule<NotifyService>
     {
         [Cmd]
         [OwnerOnly]
-        public async Task Notify(NotifyEvent nEvent, [Leftover] string message = null)
+        public async Task Notify(NotifyType nType, [Leftover] string? message = null)
         {
             if (string.IsNullOrWhiteSpace(message))
             {
-                await _service.DisableAsync(ctx.Guild.Id, nEvent);
-                await Response().Confirm(strs.notify_off(nEvent)).SendAsync();
+                await _service.DisableAsync(ctx.Guild.Id, nType);
+                await Response().Confirm(strs.notify_off(nType)).SendAsync();
                 return;
             }
 
-            await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nEvent, message);
-            await Response().Confirm(strs.notify_on(nEvent.ToString())).SendAsync();
+            await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message);
+            await Response().Confirm(strs.notify_on(nType.ToString())).SendAsync();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs b/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs
new file mode 100644
index 0000000..48bfd3c
--- /dev/null
+++ b/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs
@@ -0,0 +1,6 @@
+namespace EllieBot.Modules.Administration;
+
+public static class NotifyKeys
+{
+    public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyService.cs b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs
new file mode 100644
index 0000000..892dde9
--- /dev/null
+++ b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs
@@ -0,0 +1,202 @@
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using EllieBot.Common.ModuleBehaviors;
+using EllieBot.Db.Models;
+
+namespace EllieBot.Modules.Administration;
+
+public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
+{
+    private readonly DbService _db;
+    private readonly IMessageSenderService _mss;
+    private readonly DiscordSocketClient _client;
+    private readonly IBotCreds _creds;
+    private readonly IReplacementService _repSvc;
+    private readonly IPubSub _pubSub;
+    private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();
+
+    public NotifyService(
+        DbService db,
+        IMessageSenderService mss,
+        DiscordSocketClient client,
+        IBotCreds creds,
+        IReplacementService repSvc,
+        IPubSub pubSub)
+    {
+        _db = db;
+        _mss = mss;
+        _client = client;
+        _creds = creds;
+        _repSvc = repSvc;
+        _pubSub = pubSub;
+    }
+
+    public async Task OnReadyAsync()
+    {
+        await using var uow = _db.GetDbContext();
+        _events = (await uow.GetTable<Notify>()
+                            .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
+                                _creds.TotalShards,
+                                _client.ShardId))
+                            .ToListAsyncLinqToDB())
+                  .GroupBy(x => x.Type)
+                  .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
+                  .ToConcurrent();
+
+
+        await SubscribeToEvent<LevelUpNotifyModel>();
+    }
+
+    private async Task SubscribeToEvent<T>()
+        where T : struct, INotifyModel
+    {
+        await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
+    }
+
+    public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
+        where T : struct, INotifyModel
+    {
+        try
+        {
+            if (isShardLocal)
+            {
+                await OnEvent(data);
+                return;
+            }
+
+            await _pubSub.Pub(data.GetTypedKey(), data);
+        }
+        catch (Exception ex)
+        {
+            Log.Warning(ex,
+                "Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}",
+                T.KeyName,
+                data);
+        }
+    }
+
+    private async Task OnEvent<T>(T model)
+        where T : struct, INotifyModel
+    {
+        if (_events.TryGetValue(T.NotifyType, out var subs))
+        {
+            if (model.TryGetGuildId(out var gid))
+            {
+                if (!subs.TryGetValue(gid, out var conf))
+                    return;
+
+                await HandleNotifyEvent(conf, model);
+                return;
+            }
+
+            foreach (var key in subs.Keys.ToArray())
+            {
+                if (subs.TryGetValue(key, out var notif))
+                {
+                    try
+                    {
+                        await HandleNotifyEvent(notif, model);
+                    }
+                    catch (Exception ex)
+                    {
+                        Log.Error(ex,
+                            "Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
+                            T.NotifyType,
+                            key,
+                            ex.Message);
+                    }
+
+                    await Task.Delay(500);
+                }
+            }
+        }
+    }
+
+    private async Task HandleNotifyEvent(Notify conf, INotifyModel model)
+    {
+        var guild = _client.GetGuild(conf.GuildId);
+        var channel = guild?.GetTextChannel(conf.ChannelId);
+
+        if (guild is null || channel is null)
+            return;
+
+        IUser? user = null;
+        if (model.TryGetUserId(out var userId))
+        {
+            user = guild.GetUser(userId) ?? _client.GetUser(userId);
+        }
+
+        var rctx = new ReplacementContext(guild: guild, channel: channel, user: user);
+
+        var st = SmartText.CreateFrom(conf.Message);
+        foreach (var modelRep in model.GetReplacements())
+        {
+            rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild));
+        }
+
+        st = await _repSvc.ReplaceAsync(st, rctx);
+        if (st is SmartPlainText spt)
+        {
+            await _mss.Response(channel)
+                      .Confirm(spt.Text)
+                      .SendAsync();
+            return;
+        }
+
+        await _mss.Response(channel)
+                  .Text(st)
+                  .SendAsync();
+    }
+
+    public async Task EnableAsync(
+        ulong guildId,
+        ulong channelId,
+        NotifyType nType,
+        string message)
+    {
+        await using var uow = _db.GetDbContext();
+        await uow.GetTable<Notify>()
+                 .InsertOrUpdateAsync(() => new()
+                 {
+                     GuildId = guildId,
+                     ChannelId = channelId,
+                     Type = nType,
+                     Message = message,
+                 },
+                     (_) => new()
+                     {
+                         Message = message,
+                         ChannelId = channelId
+                     },
+                     () => new()
+                     {
+                         GuildId = guildId,
+                         Type = nType
+                     });
+
+        var eventDict = _events.GetOrAdd(nType, _ => new());
+        eventDict[guildId] = new()
+        {
+            GuildId = guildId,
+            ChannelId = channelId,
+            Type = nType,
+            Message = message
+        };
+    }
+
+    public async Task DisableAsync(ulong guildId, NotifyType nType)
+    {
+        await using var uow = _db.GetDbContext();
+        var deleted = await uow.GetTable<Notify>()
+                               .Where(x => x.GuildId == guildId && x.Type == nType)
+                               .DeleteAsync();
+
+        if (deleted == 0)
+            return;
+
+        if (!_events.TryGetValue(nType, out var guildsDict))
+            return;
+
+        guildsDict.TryRemove(guildId, out _);
+    }
+}
diff --git a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs
index c72a941..2d0ba0e 100644
--- a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs
+++ b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs
@@ -5,6 +5,36 @@ using System.Threading.Channels;
 
 namespace EllieBot.Modules.Administration.Services;
 
+public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
+{
+    public static string KeyName
+        => "notify.protection";
+
+    public static NotifyType NotifyType
+        => NotifyType.Protection;
+
+    public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
+    {
+        var data = this;
+        return new Dictionary<string, Func<SocketGuild, string>>()
+        {
+            { "%event.type%", g => data.ProtType.ToString() },
+        };
+    }
+
+    public bool TryGetUserId(out ulong userId)
+    {
+        userId = UserId;
+        return true;
+    }
+
+    public bool TryGetGuildId(out ulong guildId)
+    {
+        guildId = GuildId;
+        return true;
+    }
+}
+
 public class ProtectionService : IEService
 {
     public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate
@@ -22,6 +52,7 @@ public class ProtectionService : IEService
     private readonly MuteService _mute;
     private readonly DbService _db;
     private readonly UserPunishService _punishService;
+    private readonly INotifySubscriber _notifySub;
 
     private readonly Channel<PunishQueueItem> _punishUserQueue =
         Channel.CreateUnbounded<PunishQueueItem>(new()
@@ -35,12 +66,14 @@ public class ProtectionService : IEService
         IBot bot,
         MuteService mute,
         DbService db,
-        UserPunishService punishService)
+        UserPunishService punishService,
+        INotifySubscriber notifySub)
     {
         _client = client;
         _mute = mute;
         _db = db;
         _punishService = punishService;
+        _notifySub = notifySub;
 
         var ids = client.GetGuildIds();
         using (var uow = db.GetDbContext())
@@ -175,6 +208,9 @@ public class ProtectionService : IEService
                             alts.RoleId,
                             user);
 
+                        await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
+                            ProtectionType.Alting,
+                            user.Id));
                         return;
                     }
                 }
@@ -194,6 +230,8 @@ public class ProtectionService : IEService
                     var settings = stats.AntiRaidSettings;
 
                     await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
+                    await _notifySub.NotifyAsync(
+                        new ProtectionNotifyModel(user.Guild.Id, ProtectionType.Raiding, users[0].Id));
                 }
 
                 await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
@@ -246,6 +284,10 @@ public class ProtectionService : IEService
                             settings.MuteTime,
                             settings.RoleId,
                             (IGuildUser)msg.Author);
+
+                        await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
+                            ProtectionType.Spamming,
+                            msg.Author.Id));
                     }
                 }
             }
diff --git a/src/EllieBot/Modules/Xp/BuyResult.cs b/src/EllieBot/Modules/Xp/BuyResult.cs
new file mode 100644
index 0000000..3c4c464
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/BuyResult.cs
@@ -0,0 +1,11 @@
+namespace EllieBot.Modules.Xp.Services;
+
+public enum BuyResult
+{
+    Success,
+    XpShopDisabled,
+    AlreadyOwned,
+    InsufficientFunds,
+    UnknownItem,
+    InsufficientPatronTier,
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs
index fa6b534..a386b91 100644
--- a/src/EllieBot/Modules/Xp/Xp.cs
+++ b/src/EllieBot/Modules/Xp/Xp.cs
@@ -51,26 +51,6 @@ public partial class Xp : EllieModule<XpService>
         }
     }
 
-    [Cmd]
-    [RequireContext(ContextType.Guild)]
-    public async Task XpNotify()
-    {
-        var globalSetting = _service.GetNotificationType(ctx.User);
-
-        var embed = CreateEmbed()
-                    .WithOkColor()
-                    .AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting));
-
-        await Response().Embed(embed).SendAsync();
-    }
-
-    [Cmd]
-    public async Task XpNotify(XpNotificationLocation type)
-    {
-        await _service.ChangeNotificationType(ctx.User, type);
-        await ctx.OkAsync();
-    }
-
     [Cmd]
     [RequireContext(ContextType.Guild)]
     [UserPerm(GuildPerm.Administrator)]
@@ -615,15 +595,4 @@ public partial class Xp : EllieModule<XpService>
             await _service.UseShopItemAsync(ctx.User.Id, type, key);
         }
     }
-
-    private string GetNotifLocationString(XpNotificationLocation loc)
-    {
-        if (loc == XpNotificationLocation.Channel)
-            return GetText(strs.xpn_notif_channel);
-
-        if (loc == XpNotificationLocation.Dm)
-            return GetText(strs.xpn_notif_dm);
-
-        return GetText(strs.xpn_notif_disabled);
-    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs
index 7de2285..afff7d8 100644
--- a/src/EllieBot/Modules/Xp/XpService.cs
+++ b/src/EllieBot/Modules/Xp/XpService.cs
@@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing;
 using System.Threading.Channels;
 using LinqToDB.EntityFrameworkCore;
 using LinqToDB.Tools;
+using EllieBot.Modules.Administration;
 using EllieBot.Modules.Patronage;
 using Color = SixLabors.ImageSharp.Color;
 using Exception = System.Exception;
@@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image;
 
 namespace EllieBot.Modules.Xp.Services;
 
-public interface IUserService
-{
-    Task<DiscordUser?> GetUserAsync(ulong userId);
-}
-
-public sealed class UserService : IUserService, IEService
-{
-    private readonly DbService _db;
-
-    public UserService(DbService db)
-    {
-        _db = db;
-    }
-
-    public async Task<DiscordUser> GetUserAsync(ulong userId)
-    {
-        await using var uow = _db.GetDbContext();
-        var user = await uow
-                         .GetTable<DiscordUser>()
-                         .FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
-
-        return user;
-    }
-}
-
 public class XpService : IEService, IReadyExecutor, IExecNoCommand
 {
     private readonly DbService _db;
@@ -72,6 +48,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
     private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
     private readonly IMessageSenderService _sender;
+    private readonly INotifySubscriber _notifySub;
 
     public XpService(
         DiscordSocketClient client,
@@ -87,7 +64,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         XpConfigService xpConfig,
         IPubSub pubSub,
         IPatronageService ps,
-        IMessageSenderService sender)
+        IMessageSenderService sender,
+        INotifySubscriber notifySub)
     {
         _db = db;
         _images = images;
@@ -99,6 +77,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         _xpConfig = xpConfig;
         _pubSub = pubSub;
         _sender = sender;
+        _notifySub = notifySub;
         _excludedServers = new();
         _excludedChannels = new();
         _client = client;
@@ -189,9 +168,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
             var dus = new List<DiscordUser>(globalToAdd.Count);
             var gxps = new List<UserXpStats>(globalToAdd.Count);
+            var conf = _xpConfig.Data;
             await using (var ctx = _db.GetDbContext())
             {
-                var conf = _xpConfig.Data;
                 if (conf.CurrencyPerXp > 0)
                 {
                     foreach (var user in globalToAdd)
@@ -290,8 +269,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                             du.UserId,
                             false,
                             oldLevel.Level,
-                            newLevel.Level,
-                            du.NotifyOnLevelUp));
+                            newLevel.Level));
                 }
             }
 
@@ -328,8 +306,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         ulong userId,
         bool isServer,
         long oldLevel,
-        long newLevel,
-        XpNotificationLocation notifyLoc = XpNotificationLocation.None)
+        long newLevel)
         => async () =>
         {
             if (isServer)
@@ -337,7 +314,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                 await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
             }
 
-            await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel, notifyLoc);
+            await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel);
         };
 
     private async Task HandleRewardsInternalAsync(
@@ -388,59 +365,25 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         ulong channelId,
         ulong userId,
         bool isServer,
-        long newLevel,
-        XpNotificationLocation notifyLoc)
+        long newLevel)
     {
-        if (notifyLoc == XpNotificationLocation.None)
-            return;
-
         var guild = _client.GetGuild(guildId);
         var user = guild?.GetUser(userId);
-        var ch = guild?.GetTextChannel(channelId);
 
         if (guild is null || user is null)
             return;
 
         if (isServer)
         {
-            if (notifyLoc == XpNotificationLocation.Dm)
+            var model = new LevelUpNotifyModel()
             {
-                await _sender.Response(user)
-                             .Confirm(_strings.GetText(strs.level_up_dm(user.Mention,
-                                     Format.Bold(newLevel.ToString()),
-                                     Format.Bold(guild.ToString() ?? "-")),
-                                 guild.Id))
-                             .SendAsync();
-            }
-            else // channel
-            {
-                if (ch is not null)
-                {
-                    await _sender.Response(ch)
-                                 .Confirm(_strings.GetText(strs.level_up_channel(user.Mention,
-                                         Format.Bold(newLevel.ToString())),
-                                     guild.Id))
-                                 .SendAsync();
-                }
-            }
-        }
-        else // global level
-        {
-            var chan = notifyLoc switch
-            {
-                XpNotificationLocation.Dm => (IMessageChannel)await user.CreateDMChannelAsync(),
-                XpNotificationLocation.Channel => ch,
-                _ => null
+                GuildId = guildId,
+                UserId = userId,
+                ChannelId = channelId,
+                Level = newLevel
             };
-
-            if (chan is null)
-                return;
-
-            await _sender.Response(chan)
-                         .Confirm(_strings.GetText(strs.level_up_global(user.Mention,
-                                 Format.Bold(newLevel.ToString())),
-                             guild.Id))
-                         .SendAsync();
+            await _notifySub.NotifyAsync(model, true);
+            return;
         }
     }
 
@@ -624,20 +567,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                         .ToArrayAsyncLinqToDB();
     }
 
-    public XpNotificationLocation GetNotificationType(IUser user)
-    {
-        using var uow = _db.GetDbContext();
-        return uow.GetOrCreateUser(user).NotifyOnLevelUp;
-    }
-
-    public async Task ChangeNotificationType(IUser user, XpNotificationLocation type)
-    {
-        await using var uow = _db.GetDbContext();
-        var du = uow.GetOrCreateUser(user);
-        du.NotifyOnLevelUp = type;
-        await uow.SaveChangesAsync();
-    }
-
     private Task Client_OnGuildAvailable(SocketGuild guild)
     {
         Task.Run(async () =>
@@ -1644,23 +1573,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
                      UserId = userId,
                      Xp = lvlStats.TotalXp,
                      DateAdded = DateTime.UtcNow
-                 }, (old) => new()
-                 {
-                     Xp = lvlStats.TotalXp
-                 }, () => new()
-                 {
-                     GuildId = guildId,
-                     UserId = userId
-                 });
+                 },
+                     (old) => new()
+                     {
+                         Xp = lvlStats.TotalXp
+                     },
+                     () => new()
+                     {
+                         GuildId = guildId,
+                         UserId = userId
+                     });
     }
-}
-
-public enum BuyResult
-{
-    Success,
-    XpShopDisabled,
-    AlreadyOwned,
-    InsufficientFunds,
-    UnknownItem,
-    InsufficientPatronTier,
 }
\ No newline at end of file
diff --git a/src/EllieBot/_common/Services/IUserService.cs b/src/EllieBot/_common/Services/IUserService.cs
new file mode 100644
index 0000000..a1757e2
--- /dev/null
+++ b/src/EllieBot/_common/Services/IUserService.cs
@@ -0,0 +1,8 @@
+using EllieBot.Db.Models;
+
+namespace EllieBot.Modules.Xp.Services;
+
+public interface IUserService
+{
+    Task<DiscordUser?> GetUserAsync(ulong userId);
+}
\ No newline at end of file
diff --git a/src/EllieBot/_common/Services/UserService.cs b/src/EllieBot/_common/Services/UserService.cs
new file mode 100644
index 0000000..7d5ad70
--- /dev/null
+++ b/src/EllieBot/_common/Services/UserService.cs
@@ -0,0 +1,24 @@
+using LinqToDB.EntityFrameworkCore;
+using EllieBot.Db.Models;
+
+namespace EllieBot.Modules.Xp.Services;
+
+public sealed class UserService : IUserService, IEService
+{
+    private readonly DbService _db;
+
+    public UserService(DbService db)
+    {
+        _db = db;
+    }
+
+    public async Task<DiscordUser> GetUserAsync(ulong userId)
+    {
+        await using var uow = _db.GetDbContext();
+        var user = await uow
+                         .GetTable<DiscordUser>()
+                         .FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
+
+        return user;
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml
index 0f9f9fe..f43750e 100644
--- a/src/EllieBot/data/aliases.yml
+++ b/src/EllieBot/data/aliases.yml
@@ -1546,4 +1546,7 @@ minesweeper:
   - minesweeper
   - mw
 temprole:
-  - temprole
\ No newline at end of file
+  - temprole
+notify:
+  - notify
+  - nfy
\ No newline at end of file
diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml
index 384d00b..68d1348 100644
--- a/src/EllieBot/data/strings/commands/commands.en-US.yml
+++ b/src/EllieBot/data/strings/commands/commands.en-US.yml
@@ -4853,4 +4853,14 @@ minesweeper:
     - '15'
   params:
     - mines:
-        desc: "The number of mines to create."
\ No newline at end of file
+        desc: "The number of mines to create."
+notify:
+  desc: |-
+    Sends a message to the current channel once the specified event occurs.
+  ex:
+    - 'levelup Congratulations to user %user.name% for reaching level %event.level%'
+  params:
+    - event:
+        desc: "The event to notify on."
+    - message:
+        desc: "The message to send."
\ No newline at end of file