diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
deleted file mode 100644
index 921f7f92d9..0000000000
--- a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-  "db_name": "PostgreSQL",
-  "query": "\n                SELECT\n                    id,\n                    status AS \"status: PayoutStatus\"\n                FROM payouts\n                ORDER BY id\n                ",
-  "describe": {
-    "columns": [
-      {
-        "ordinal": 0,
-        "name": "id",
-        "type_info": "Int8"
-      },
-      {
-        "ordinal": 1,
-        "name": "status: PayoutStatus",
-        "type_info": "Varchar"
-      }
-    ],
-    "parameters": {
-      "Left": []
-    },
-    "nullable": [
-      false,
-      false
-    ]
-  },
-  "hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
-}
diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
deleted file mode 100644
index 89bd8147dc..0000000000
--- a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  "db_name": "PostgreSQL",
-  "query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
-  "describe": {
-    "columns": [
-      {
-        "ordinal": 0,
-        "name": "status: PayoutStatus",
-        "type_info": "Varchar"
-      }
-    ],
-    "parameters": {
-      "Left": []
-    },
-    "nullable": [
-      false
-    ]
-  },
-  "hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
-}
diff --git a/apps/labrinth/.sqlx/query-807d42d4aab8312fc6dfc60c93c6cecf947c2f96b43926ce5147c4aafe9089cd.json b/apps/labrinth/.sqlx/query-ccdee0f8f60e0ac8997eacba6993ed0c61cd80c0d9eacd3053fadd09f7c67e6d.json
similarity index 73%
rename from apps/labrinth/.sqlx/query-807d42d4aab8312fc6dfc60c93c6cecf947c2f96b43926ce5147c4aafe9089cd.json
rename to apps/labrinth/.sqlx/query-ccdee0f8f60e0ac8997eacba6993ed0c61cd80c0d9eacd3053fadd09f7c67e6d.json
index 9380c69df1..ad2796d0f4 100644
--- a/apps/labrinth/.sqlx/query-807d42d4aab8312fc6dfc60c93c6cecf947c2f96b43926ce5147c4aafe9089cd.json
+++ b/apps/labrinth/.sqlx/query-ccdee0f8f60e0ac8997eacba6993ed0c61cd80c0d9eacd3053fadd09f7c67e6d.json
@@ -1,8 +1,19 @@
 {
   "db_name": "PostgreSQL",
-  "query": "\n            WITH\n              period_payouts AS (\n                SELECT\n                  ids.notification_id,\n                  ids.user_id,\n                  ids.date_available,\n                  FLOOR(COALESCE(SUM(pv.amount), 0.0) * 100) :: BIGINT sum -- Convert to cents\n                  FROM UNNEST($1::bigint[], $2::bigint[], $3::timestamptz[]) AS ids(notification_id, user_id, date_available)\n                LEFT JOIN payouts_values pv ON pv.user_id = ids.user_id AND pv.date_available = ids.date_available\n                GROUP BY ids.user_id, ids.notification_id, ids.date_available\n              )\n            INSERT INTO notifications (\n                id, user_id, body\n            )\n            SELECT\n              notification_id id,\n              user_id,\n              JSONB_BUILD_OBJECT(\n                'type', 'payout_available',\n                'date_available', to_jsonb(date_available),\n                'amount', to_jsonb(sum)\n              ) body\n            FROM period_payouts\n            WHERE sum >= 100\n            ",
+  "query": "\n            WITH\n              period_payouts AS (\n                SELECT\n                  ids.notification_id,\n                  ids.user_id,\n                  ids.date_available,\n                  FLOOR(COALESCE(SUM(pv.amount), 0.0) * 100) :: BIGINT sum -- Convert to cents\n                  FROM UNNEST($1::bigint[], $2::bigint[], $3::timestamptz[]) AS ids(notification_id, user_id, date_available)\n                LEFT JOIN payouts_values pv ON pv.user_id = ids.user_id AND pv.date_available = ids.date_available\n                GROUP BY ids.user_id, ids.notification_id, ids.date_available\n              )\n            INSERT INTO notifications (\n                id, user_id, body\n            )\n            SELECT\n              notification_id id,\n              user_id,\n              JSONB_BUILD_OBJECT(\n                'type', 'payout_available',\n                'date_available', to_jsonb(date_available),\n                'amount', to_jsonb(sum)\n              ) body\n            FROM period_payouts\n            WHERE sum >= 100\n            RETURNING id, user_id\n            ",
   "describe": {
-    "columns": [],
+    "columns": [
+      {
+        "ordinal": 0,
+        "name": "id",
+        "type_info": "Int8"
+      },
+      {
+        "ordinal": 1,
+        "name": "user_id",
+        "type_info": "Int8"
+      }
+    ],
     "parameters": {
       "Left": [
         "Int8Array",
@@ -10,7 +21,10 @@
         "TimestamptzArray"
       ]
     },
-    "nullable": []
+    "nullable": [
+      false,
+      false
+    ]
   },
-  "hash": "807d42d4aab8312fc6dfc60c93c6cecf947c2f96b43926ce5147c4aafe9089cd"
+  "hash": "ccdee0f8f60e0ac8997eacba6993ed0c61cd80c0d9eacd3053fadd09f7c67e6d"
 }
diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
deleted file mode 100644
index 469c30168a..0000000000
--- a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "db_name": "PostgreSQL",
-  "query": "\n                INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n                VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n                ",
-  "describe": {
-    "columns": [],
-    "parameters": {
-      "Left": [
-        "Int8",
-        "Text",
-        "Text",
-        "Varchar",
-        "Int8"
-      ]
-    },
-    "nullable": []
-  },
-  "hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
-}
diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
deleted file mode 100644
index 52e020ebf2..0000000000
--- a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "db_name": "PostgreSQL",
-  "query": "\n                    INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n                    VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n                    ",
-  "describe": {
-    "columns": [],
-    "parameters": {
-      "Left": [
-        "Int8",
-        "Text",
-        "Varchar",
-        "Int8"
-      ]
-    },
-    "nullable": []
-  },
-  "hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
-}
diff --git a/apps/labrinth/src/database/models/notification_item.rs b/apps/labrinth/src/database/models/notification_item.rs
index ae01076337..c25858ef29 100644
--- a/apps/labrinth/src/database/models/notification_item.rs
+++ b/apps/labrinth/src/database/models/notification_item.rs
@@ -57,7 +57,13 @@ impl NotificationBuilder {
         let notification_ids =
             notification_ids.iter().map(|x| x.0).collect::<Vec<_>>();
 
-        sqlx::query!(
+        // Use RETURNING to get back only the notification_ids and user_ids
+        // that were actually inserted (i.e. those with sum >= 100).
+        // This is necessary because insert_many_deliveries references
+        // notifications(id) via a foreign key, passing notification_ids
+        // that were filtered out by the WHERE clause would cause a
+        // FK constraint violation and fail the entire transaction.
+        let inserted_rows = sqlx::query!(
             "
             WITH
               period_payouts AS (
@@ -83,15 +89,29 @@ impl NotificationBuilder {
               ) body
             FROM period_payouts
             WHERE sum >= 100
+            RETURNING id, user_id
             ",
             &notification_ids[..],
             &users_raw_ids[..],
             &dates_available[..],
         )
-        .execute(&mut *transaction)
+        .fetch_all(&mut *transaction)
         .await?;
 
-        let notification_types = notification_ids
+        if inserted_rows.is_empty() {
+            return Ok(());
+        }
+
+        let inserted_notification_ids: Vec<i64> =
+            inserted_rows.iter().map(|r| r.id).collect();
+        let inserted_user_raw_ids: Vec<i64> =
+            inserted_rows.iter().map(|r| r.user_id).collect();
+        let inserted_users: Vec<DBUserId> = inserted_user_raw_ids
+            .iter()
+            .map(|&id| DBUserId(id))
+            .collect();
+
+        let notification_types = inserted_notification_ids
             .iter()
             .map(|_| NotificationType::PayoutAvailable.as_str())
             .collect::<Vec<_>>();
@@ -99,10 +119,10 @@ impl NotificationBuilder {
         NotificationBuilder::insert_many_deliveries(
             transaction,
             redis,
-            &notification_ids,
-            &users_raw_ids,
+            &inserted_notification_ids,
+            &inserted_user_raw_ids,
             &notification_types,
-            &users,
+            &inserted_users,
         )
         .await?;
 
diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs
index cd6c38b8a6..5918424a40 100644
--- a/apps/labrinth/src/queue/payouts/mod.rs
+++ b/apps/labrinth/src/queue/payouts/mod.rs
@@ -1405,3 +1405,179 @@ async fn check_balance_with_webhook(
 
     Ok(result.ok().flatten())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test::{
+        api_v3::ApiV3,
+        database::USER_USER_ID_PARSED,
+        environment::{TestEnvironment, with_test_environment},
+    };
+    use rust_decimal::dec;
+
+    async fn setup_payouts_values(
+        db: &PgPool,
+        entries: Vec<(i64, Decimal, DateTime<Utc>)>, // (user_id, amount, date_available)
+    ) {
+        for (user_id, amount, date_available) in &entries {
+            sqlx::query!(
+                "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)
+                 VALUES ($1, NULL, $2, NOW(), $3)",
+                user_id,
+                amount,
+                date_available,
+            )
+                .execute(db)
+                .await
+                .unwrap();
+        }
+
+        for (user_id, _amount, date_available) in &entries {
+            sqlx::query!(
+                "INSERT INTO payouts_values_notifications (date_available, user_id, notified)
+                 VALUES ($1, $2, FALSE)
+                 ON CONFLICT (date_available, user_id) DO NOTHING",
+                date_available,
+                user_id,
+            )
+                .execute(db)
+                .await
+                .unwrap();
+        }
+    }
+
+    /// When a user's payout amount is below
+    /// the $1.00 (100 cents) threshold, the `WHERE sum >= 100` filter in
+    /// insert_many_payout_notifications skips the INSERT into notifications
+    /// for that user, but the old code still passed ALL pre-generated
+    /// notification_ids to insert_many_deliveries. Since
+    /// notifications_deliveries.notification_id has a FK constraint on
+    /// notifications(id), this caused a violation that failed
+    /// the entire transaction. The notified flag was never set to `true`,
+    /// so the rows accumulated and the same failure repeated every run.
+    #[actix_rt::test]
+    async fn test_payout_notification_below_threshold_does_not_fail() {
+        with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
+            let db = &env.db.pool;
+            let redis = &env.db.redis_pool;
+
+            let user_id = USER_USER_ID_PARSED;
+
+            // date_available must be in the past so the notification query
+            // picks it up (date_available <= NOW()).
+            let date_available = Utc::now() - Duration::hours(1);
+
+            // Amount of $0.50 -- below the $1.00 threshold (sum < 100 cents).
+            setup_payouts_values(
+                db,
+                vec![(user_id, dec!(0.50), date_available)],
+            )
+                .await;
+
+            // This should succeed, NOT return an error.
+            // Before the fix, this would fail with a FK constraint violation.
+            let result = index_payouts_notifications(db, redis).await;
+            assert!(
+                result.is_ok(),
+                "index_payouts_notifications should succeed for below-threshold payouts, got: {:?}",
+                result.err()
+            );
+
+            // Verify the notification row was marked as notified (not stuck).
+            let remaining = sqlx::query_scalar!(
+                "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
+                user_id,
+            )
+                .fetch_one(db)
+                .await
+                .unwrap()
+                .unwrap_or(0);
+
+            assert_eq!(
+                remaining, 0,
+                "All payouts_values_notifications rows should be marked notified"
+            );
+        })
+            .await;
+    }
+
+    /// When there is a mix of users, some above and some below the
+    /// threshold, the above-threshold users should get notifications
+    /// while the below-threshold ones are silently skipped. The entire
+    /// transaction must succeed (not fail due to the below-threshold user).
+    #[actix_rt::test]
+    async fn test_payout_notification_mixed_threshold_users() {
+        with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
+            let db = &env.db.pool;
+            let redis = &env.db.redis_pool;
+
+            let above_user_id = USER_USER_ID_PARSED; // user 3
+            let below_user_id = 4i64; // FRIEND_USER_ID
+            let date_available = Utc::now() - Duration::hours(1);
+
+            setup_payouts_values(
+                db,
+                vec![
+                    // Above threshold
+                    (above_user_id, dec!(5.00), date_available),
+                    // Below threshold
+                    (below_user_id, dec!(0.25), date_available),
+                ],
+            )
+                .await;
+
+            let result = index_payouts_notifications(db, redis).await;
+            assert!(
+                result.is_ok(),
+                "index_payouts_notifications should succeed with mixed users, got: {:?}",
+                result.err()
+            );
+
+            // Above-threshold user should have a notification.
+            let above_count = sqlx::query_scalar!(
+                "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
+                above_user_id,
+            )
+                .fetch_one(db)
+                .await
+                .unwrap()
+                .unwrap_or(0);
+
+            assert!(
+                above_count > 0,
+                "Above-threshold user should have a payout notification"
+            );
+
+            // Below-threshold user should NOT have a notification.
+            let below_count = sqlx::query_scalar!(
+                "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
+                below_user_id,
+            )
+                .fetch_one(db)
+                .await
+                .unwrap()
+                .unwrap_or(0);
+
+            assert_eq!(
+                below_count, 0,
+                "Below-threshold user should NOT have a payout notification"
+            );
+
+            // Both should be marked notified.
+            let remaining = sqlx::query_scalar!(
+                "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
+            )
+                .fetch_one(db)
+                .await
+                .unwrap()
+                .unwrap_or(0);
+
+            assert_eq!(
+                remaining, 0,
+                "All payouts_values_notifications rows should be marked notified"
+            );
+        })
+            .await;
+    }
+}
