diff --git a/src/consensus/validation.h b/src/consensus/validation.h
--- a/src/consensus/validation.h
+++ b/src/consensus/validation.h
@@ -42,6 +42,11 @@
     TX_MEMPOOL_POLICY,
     //! this node does not have a mempool so can't validate the transaction
     TX_NO_MEMPOOL,
+    //! fails some policy, but might be acceptable if submitted in a (different)
+    //! package
+    TX_RECONSIDERABLE,
+    //! transaction was not validated because package failed
+    TX_UNKNOWN,
 };
 
 /**
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -2557,6 +2557,8 @@
         case TxValidationResult::TX_CHILD_BEFORE_PARENT:
         case TxValidationResult::TX_MEMPOOL_POLICY:
         case TxValidationResult::TX_NO_MEMPOOL:
+        case TxValidationResult::TX_RECONSIDERABLE:
+        case TxValidationResult::TX_UNKNOWN:
             break;
     }
     if (message != "") {
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -55,6 +55,7 @@
 	util/setup_common.cpp
 	util/str.cpp
 	util/transaction_utils.cpp
+	util/txmempool.cpp
 	util/validation.cpp
 	util/wallet.cpp
 )
diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp
--- a/src/test/txpackage_tests.cpp
+++ b/src/test/txpackage_tests.cpp
@@ -14,6 +14,7 @@
 
 #include <test/util/random.h>
 #include <test/util/setup_common.h>
+#include <test/util/txmempool.h>
 
 #include <boost/test/unit_test.hpp>
 
@@ -99,59 +100,60 @@
         /* output_destination */ child_locking_script,
         /* output_amount */ Amount(48 * COIN), /* submit */ false);
     CTransactionRef tx_child = MakeTransactionRef(mtx_child);
+    Package package_parent_child{tx_parent, tx_child};
     const auto result_parent_child =
         ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
-                          {tx_parent, tx_child}, /* test_accept */ true);
-    BOOST_CHECK_MESSAGE(result_parent_child.m_state.IsValid(),
-                        "Package validation unexpectedly failed: "
-                            << result_parent_child.m_state.GetRejectReason());
-    BOOST_CHECK(result_parent_child.m_tx_results.size() == 2);
-    auto it_parent = result_parent_child.m_tx_results.find(tx_parent->GetId());
-    auto it_child = result_parent_child.m_tx_results.find(tx_child->GetId());
-    BOOST_CHECK(it_parent != result_parent_child.m_tx_results.end());
-    BOOST_CHECK_MESSAGE(it_parent->second.m_state.IsValid(),
-                        "Package validation unexpectedly failed: "
-                            << it_parent->second.m_state.GetRejectReason());
-    BOOST_CHECK_EQUAL(
-        it_parent->second.m_effective_feerate.value().GetFeeCeiling(
-            GetVirtualTransactionSize(*tx_parent)),
-        COIN);
-    BOOST_CHECK_EQUAL(it_parent->second.m_txids_fee_calculations.value().size(),
-                      1);
-    BOOST_CHECK_EQUAL(
-        it_parent->second.m_txids_fee_calculations.value().front(),
-        tx_parent->GetId());
-    BOOST_CHECK(it_child != result_parent_child.m_tx_results.end());
-    BOOST_CHECK_MESSAGE(it_child->second.m_state.IsValid(),
-                        "Package validation unexpectedly failed: "
-                            << it_child->second.m_state.GetRejectReason());
-    BOOST_CHECK_EQUAL(
-        it_child->second.m_effective_feerate.value().GetFeeCeiling(
-            GetVirtualTransactionSize(*tx_child)),
-        COIN);
-    BOOST_CHECK_EQUAL(it_child->second.m_txids_fee_calculations.value().size(),
-                      1);
-    BOOST_CHECK_EQUAL(it_child->second.m_txids_fee_calculations.value().front(),
-                      tx_child->GetId());
+                          package_parent_child, /*test_accept=*/true);
+    if (auto err_parent_child{CheckPackageMempoolAcceptResult(
+            package_parent_child, result_parent_child, /*expect_valid=*/true,
+            nullptr)}) {
+        BOOST_ERROR(err_parent_child.value());
+    } else {
+        auto it_parent =
+            result_parent_child.m_tx_results.find(tx_parent->GetId());
+        auto it_child =
+            result_parent_child.m_tx_results.find(tx_child->GetId());
+
+        BOOST_CHECK(it_parent->second.m_effective_feerate.value().GetFeeCeiling(
+                        GetVirtualTransactionSize(*tx_parent)) == COIN);
+        BOOST_CHECK_EQUAL(
+            it_parent->second.m_txids_fee_calculations.value().size(), 1);
+        BOOST_CHECK_EQUAL(
+            it_parent->second.m_txids_fee_calculations.value().front(),
+            tx_parent->GetId());
+
+        BOOST_CHECK(it_child->second.m_effective_feerate.value().GetFeeCeiling(
+                        GetVirtualTransactionSize(*tx_child)) == COIN);
+        BOOST_CHECK_EQUAL(
+            it_child->second.m_txids_fee_calculations.value().size(), 1);
+        BOOST_CHECK_EQUAL(
+            it_child->second.m_txids_fee_calculations.value().front(),
+            tx_child->GetId());
+    }
 
     // A single, giant transaction submitted through ProcessNewPackage fails on
     // single tx policy.
     CTransactionRef giant_ptx = create_placeholder_tx(999, 999);
     BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) >
                 MAX_PACKAGE_SIZE * 1000);
+    Package package_single_giant{giant_ptx};
     auto result_single_large =
         ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
-                          {giant_ptx}, /* test_accept */ true);
-    BOOST_CHECK(result_single_large.m_state.IsInvalid());
-    BOOST_CHECK_EQUAL(result_single_large.m_state.GetResult(),
-                      PackageValidationResult::PCKG_TX);
-    BOOST_CHECK_EQUAL(result_single_large.m_state.GetRejectReason(),
-                      "transaction failed");
-    BOOST_CHECK(result_single_large.m_tx_results.size() == 1);
-    auto it_giant_tx =
-        result_single_large.m_tx_results.find(giant_ptx->GetId());
-    BOOST_CHECK(it_giant_tx != result_single_large.m_tx_results.end());
-    BOOST_CHECK_EQUAL(it_giant_tx->second.m_state.GetRejectReason(), "tx-size");
+                          package_single_giant, /*test_accept=*/true);
+    if (auto err_single_large{CheckPackageMempoolAcceptResult(
+            package_single_giant, result_single_large, /*expect_valid=*/false,
+            nullptr)}) {
+        BOOST_ERROR(err_single_large.value());
+    } else {
+        BOOST_CHECK_EQUAL(result_single_large.m_state.GetResult(),
+                          PackageValidationResult::PCKG_TX);
+        BOOST_CHECK_EQUAL(result_single_large.m_state.GetRejectReason(),
+                          "transaction failed");
+        auto it_giant_tx =
+            result_single_large.m_tx_results.find(giant_ptx->GetId());
+        BOOST_CHECK_EQUAL(it_giant_tx->second.m_state.GetRejectReason(),
+                          "tx-size");
+    }
 
     // Check that mempool size hasn't changed.
     BOOST_CHECK_EQUAL(m_node.mempool->size(), initialPoolSize);
@@ -285,6 +287,8 @@
         auto result_unrelated_submit = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool,
             package_unrelated, /*test_accept=*/false);
+        // We don't expect m_tx_results for each transaction when basic sanity
+        // checks haven't passed.
         BOOST_CHECK(result_unrelated_submit.m_state.IsInvalid());
         BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetResult(),
                           PackageValidationResult::PCKG_POLICY);
@@ -363,32 +367,34 @@
         CTransactionRef tx_child_of_invalid_parent =
             MakeTransactionRef(mtx_child_of_invalid_parent);
 
+        Package package_invalid_parent{tx_parent_invalid,
+                                       tx_child_of_invalid_parent};
         auto result_quit_early = WITH_LOCK(
             cs_main, return ProcessNewPackage(
                          m_node.chainman->ActiveChainstate(), *m_node.mempool,
-                         {tx_parent_invalid, tx_child_of_invalid_parent},
-                         /*test_accept=*/false));
-        BOOST_CHECK(result_quit_early.m_state.IsInvalid());
+                         package_invalid_parent, /*test_accept=*/false));
+        if (auto err_parent_invalid{CheckPackageMempoolAcceptResult(
+                package_invalid_parent, result_quit_early,
+                /*expect_valid=*/false, m_node.mempool.get())}) {
+            BOOST_ERROR(err_parent_invalid.value());
+        } else {
+            auto it_parent =
+                result_quit_early.m_tx_results.find(tx_parent_invalid->GetId());
+            auto it_child = result_quit_early.m_tx_results.find(
+                tx_child_of_invalid_parent->GetId());
+            BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(),
+                              TxValidationResult::TX_CONSENSUS);
+            BOOST_CHECK_EQUAL(
+                it_parent->second.m_state.GetRejectReason(),
+                "mandatory-script-verify-flag-failed (Script evaluated without "
+                "error but finished with a false/empty top stack element)");
+            BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(),
+                              TxValidationResult::TX_MISSING_INPUTS);
+            BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(),
+                              "bad-txns-inputs-missingorspent");
+        }
         BOOST_CHECK_EQUAL(result_quit_early.m_state.GetResult(),
                           PackageValidationResult::PCKG_TX);
-        BOOST_CHECK(!result_quit_early.m_tx_results.empty());
-        BOOST_CHECK_EQUAL(result_quit_early.m_tx_results.size(), 2);
-        auto it_parent =
-            result_quit_early.m_tx_results.find(tx_parent_invalid->GetId());
-        auto it_child = result_quit_early.m_tx_results.find(
-            tx_child_of_invalid_parent->GetId());
-        BOOST_CHECK(it_parent != result_quit_early.m_tx_results.end());
-        BOOST_CHECK(it_child != result_quit_early.m_tx_results.end());
-        BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(),
-                          TxValidationResult::TX_CONSENSUS);
-        BOOST_CHECK_EQUAL(
-            it_parent->second.m_state.GetRejectReason(),
-            "mandatory-script-verify-flag-failed (Script evaluated without "
-            "error but finished with a false/empty top stack element)");
-        BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(),
-                          TxValidationResult::TX_MISSING_INPUTS);
-        BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(),
-                          "bad-txns-inputs-missingorspent");
     }
 
     // Child with missing parent.
@@ -435,8 +441,6 @@
         BOOST_CHECK_EQUAL(
             it_parent->second.m_txids_fee_calculations.value().front(),
             tx_parent->GetId());
-        BOOST_CHECK(it_child != submit_parent_child.m_tx_results.end());
-        BOOST_CHECK(it_child->second.m_state.IsValid());
         BOOST_CHECK(it_child->second.m_effective_feerate ==
                     CFeeRate(1 * COIN, GetVirtualTransactionSize(*tx_child)));
         BOOST_CHECK_EQUAL(
@@ -446,8 +450,6 @@
             tx_child->GetId());
 
         BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-        BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId()));
-        BOOST_CHECK(m_node.mempool->exists(tx_child->GetId()));
     }
 
     // Already-in-mempool transactions should be detected and de-duplicated.
@@ -456,27 +458,21 @@
         const auto submit_deduped = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool,
             package_parent_child, /*test_accept=*/false);
-        BOOST_CHECK_MESSAGE(submit_deduped.m_state.IsValid(),
-                            "Package validation unexpectedly failed: "
-                                << submit_deduped.m_state.GetRejectReason());
-        BOOST_CHECK_EQUAL(submit_deduped.m_tx_results.size(),
-                          package_parent_child.size());
-        auto it_parent_deduped =
-            submit_deduped.m_tx_results.find(tx_parent->GetId());
-        auto it_child_deduped =
-            submit_deduped.m_tx_results.find(tx_child->GetId());
-        BOOST_CHECK(it_parent_deduped != submit_deduped.m_tx_results.end());
-        BOOST_CHECK(it_parent_deduped->second.m_state.IsValid());
-        BOOST_CHECK(it_parent_deduped->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
-        BOOST_CHECK(it_child_deduped != submit_deduped.m_tx_results.end());
-        BOOST_CHECK(it_child_deduped->second.m_state.IsValid());
-        BOOST_CHECK(it_child_deduped->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
-
+        if (auto err_deduped{CheckPackageMempoolAcceptResult(
+                package_parent_child, submit_deduped, /*expect_valid=*/true,
+                m_node.mempool.get())}) {
+            BOOST_ERROR(err_deduped.value());
+        } else {
+            auto it_parent_deduped =
+                submit_deduped.m_tx_results.find(tx_parent->GetId());
+            auto it_child_deduped =
+                submit_deduped.m_tx_results.find(tx_child->GetId());
+            BOOST_CHECK(it_parent_deduped->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
+            BOOST_CHECK(it_child_deduped->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
+        }
         BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-        BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId()));
-        BOOST_CHECK(m_node.mempool->exists(tx_child->GetId()));
     }
 }
 
@@ -543,44 +539,41 @@
         const auto mixed_result =
             ProcessNewPackage(m_node.chainman->ActiveChainstate(),
                               *m_node.mempool, package_mixed, false);
-        BOOST_CHECK_MESSAGE(mixed_result.m_state.IsValid(),
-                            mixed_result.m_state.GetRejectReason());
-        BOOST_CHECK_EQUAL(mixed_result.m_tx_results.size(),
-                          package_mixed.size());
-        auto it_parent1 = mixed_result.m_tx_results.find(ptx_parent1->GetId());
-        auto it_parent2 = mixed_result.m_tx_results.find(ptx_parent2->GetId());
-        auto it_child =
-            mixed_result.m_tx_results.find(ptx_mixed_child->GetId());
-        BOOST_CHECK(it_parent1 != mixed_result.m_tx_results.end());
-        BOOST_CHECK(it_parent2 != mixed_result.m_tx_results.end());
-        BOOST_CHECK(it_child != mixed_result.m_tx_results.end());
-
-        BOOST_CHECK(it_parent1->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
-        BOOST_CHECK(it_parent2->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_child->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-
-        BOOST_CHECK(m_node.mempool->exists(ptx_parent1->GetId()));
-        BOOST_CHECK(m_node.mempool->exists(ptx_parent2->GetId()));
-        BOOST_CHECK(m_node.mempool->exists(ptx_mixed_child->GetId()));
-
-        // package feerate should include parent2 and child. It should not
-        // include parent1.
-        const CFeeRate expected_feerate(
-            1 * COIN, GetVirtualTransactionSize(*ptx_parent2) +
-                          GetVirtualTransactionSize(*ptx_mixed_child));
-        BOOST_CHECK(it_parent2->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        std::vector<TxId> expected_txids(
-            {ptx_parent2->GetId(), ptx_mixed_child->GetId()});
-        BOOST_CHECK(it_parent2->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
-        BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
+        if (auto err_mixed{CheckPackageMempoolAcceptResult(
+                package_mixed, mixed_result, /*expect_valid=*/true,
+                m_node.mempool.get())}) {
+            BOOST_ERROR(err_mixed.value());
+        } else {
+            auto it_parent1 =
+                mixed_result.m_tx_results.find(ptx_parent1->GetId());
+            auto it_parent2 =
+                mixed_result.m_tx_results.find(ptx_parent2->GetId());
+            auto it_child =
+                mixed_result.m_tx_results.find(ptx_mixed_child->GetId());
+
+            BOOST_CHECK(it_parent1->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
+            BOOST_CHECK(it_parent2->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_child->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+
+            // package feerate should include parent2 and child. It should not
+            // include parent1.
+            const CFeeRate expected_feerate(
+                1 * COIN, GetVirtualTransactionSize(*ptx_parent2) +
+                              GetVirtualTransactionSize(*ptx_mixed_child));
+            BOOST_CHECK(it_parent2->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            std::vector<TxId> expected_txids(
+                {ptx_parent2->GetId(), ptx_mixed_child->GetId()});
+            BOOST_CHECK(it_parent2->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+            BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+        }
     }
 }
 
@@ -630,26 +623,27 @@
         const auto submit_cpfp_deprio = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool, package_cpfp,
             /*test_accept=*/false);
-
-        BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(),
-                          PackageValidationResult::PCKG_TX);
-        BOOST_CHECK(submit_cpfp_deprio.m_state.IsInvalid());
-        BOOST_CHECK_EQUAL(
-            submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId())
-                ->second.m_state.GetResult(),
-            TxValidationResult::TX_MEMPOOL_POLICY);
-        BOOST_CHECK_EQUAL(
-            submit_cpfp_deprio.m_tx_results.find(tx_child->GetId())
-                ->second.m_state.GetResult(),
-            TxValidationResult::TX_MISSING_INPUTS);
-        BOOST_CHECK(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId())
-                        ->second.m_state.GetRejectReason() ==
-                    "min relay fee not met");
-
-        BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-        const CFeeRate expected_feerate(
-            Amount::zero(), GetVirtualTransactionSize(*tx_parent) +
-                                GetVirtualTransactionSize(*tx_child));
+        if (auto err_cpfp_deprio{CheckPackageMempoolAcceptResult(
+                package_cpfp, submit_cpfp_deprio, /*expect_valid=*/false,
+                m_node.mempool.get())}) {
+            BOOST_ERROR(err_cpfp_deprio.value());
+        } else {
+            BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(),
+                              PackageValidationResult::PCKG_TX);
+            BOOST_CHECK_EQUAL(
+                submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId())
+                    ->second.m_state.GetResult(),
+                TxValidationResult::TX_MEMPOOL_POLICY);
+            BOOST_CHECK_EQUAL(
+                submit_cpfp_deprio.m_tx_results.find(tx_child->GetId())
+                    ->second.m_state.GetResult(),
+                TxValidationResult::TX_MISSING_INPUTS);
+            BOOST_CHECK(
+                submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId())
+                    ->second.m_state.GetRejectReason() ==
+                "min relay fee not met");
+            BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
+        }
     }
 
     // Clear the prioritisation of the parent transaction.
@@ -664,42 +658,39 @@
         const auto submit_cpfp = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool, package_cpfp,
             /*test_accept=*/false);
+        if (auto err_cpfp{CheckPackageMempoolAcceptResult(
+                package_cpfp, submit_cpfp, /*expect_valid=*/true,
+                m_node.mempool.get())}) {
+            BOOST_ERROR(err_cpfp.value());
+        } else {
+            auto it_parent = submit_cpfp.m_tx_results.find(tx_parent->GetId());
+            auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetId());
+            BOOST_CHECK(it_parent->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_parent->second.m_base_fees.value() ==
+                        coinbase_value - parent_value);
+            BOOST_CHECK(it_child->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_child->second.m_base_fees.value() == COIN);
+
+            const CFeeRate expected_feerate(
+                coinbase_value - child_value,
+                GetVirtualTransactionSize(*tx_parent) +
+                    GetVirtualTransactionSize(*tx_child));
+            BOOST_CHECK(it_parent->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            std::vector<TxId> expected_txids(
+                {tx_parent->GetId(), tx_child->GetId()});
+            BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+            BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+            BOOST_CHECK(expected_feerate.GetFeePerK() > 1000 * SATOSHI);
+        }
         expected_pool_size += 2;
-        BOOST_CHECK_MESSAGE(submit_cpfp.m_state.IsValid(),
-                            "Package validation unexpectedly failed: "
-                                << submit_cpfp.m_state.GetRejectReason());
-        BOOST_CHECK_EQUAL(submit_cpfp.m_tx_results.size(), package_cpfp.size());
-        auto it_parent = submit_cpfp.m_tx_results.find(tx_parent->GetId());
-        auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetId());
-        BOOST_CHECK(it_parent != submit_cpfp.m_tx_results.end());
-        BOOST_CHECK(it_parent->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_parent->second.m_base_fees.value() ==
-                    coinbase_value - parent_value);
-        BOOST_CHECK(it_child != submit_cpfp.m_tx_results.end());
-        BOOST_CHECK(it_child->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_child->second.m_base_fees.value() == COIN);
-
         BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-        BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId()));
-        BOOST_CHECK(m_node.mempool->exists(tx_child->GetId()));
-
-        const CFeeRate expected_feerate(
-            coinbase_value - child_value,
-            GetVirtualTransactionSize(*tx_parent) +
-                GetVirtualTransactionSize(*tx_child));
-        BOOST_CHECK(it_parent->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        std::vector<TxId> expected_txids(
-            {tx_parent->GetId(), tx_child->GetId()});
-        BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
-        BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
-        BOOST_CHECK(expected_feerate.GetFeePerK() > 1000 * SATOSHI);
     }
 
     // Just because we allow low-fee parents doesn't mean we allow low-feerate
@@ -729,26 +720,50 @@
         /*submit=*/false);
     CTransactionRef tx_child_cheap = MakeTransactionRef(mtx_child_cheap);
     package_still_too_low.push_back(tx_child_cheap);
-    std::cout << GetVirtualTransactionSize(*tx_child_cheap) << std::endl;
     BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(
                     GetVirtualTransactionSize(*tx_child_cheap)) <= child_fee);
     BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(
                     GetVirtualTransactionSize(*tx_parent_cheap) +
                     GetVirtualTransactionSize(*tx_child_cheap)) >
                 parent_fee + child_fee);
+    BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
 
-    // Cheap package should fail with package-fee-too-low.
+    // Cheap package should fail for being too low fee.
     {
-        BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
         const auto submit_package_too_low = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool,
             package_still_too_low, /* test_accept */ false);
-        BOOST_CHECK_MESSAGE(submit_package_too_low.m_state.IsInvalid(),
-                            "Package validation unexpectedly succeeded");
+        if (auto err_package_too_low{CheckPackageMempoolAcceptResult(
+                package_still_too_low, submit_package_too_low,
+                /*expect_valid=*/false, m_node.mempool.get())}) {
+            BOOST_ERROR(err_package_too_low.value());
+        } else {
+            // Individual feerate of parent is too low.
+            BOOST_CHECK_EQUAL(
+                submit_package_too_low.m_tx_results.at(tx_parent_cheap->GetId())
+                    .m_state.GetResult(),
+                TxValidationResult::TX_RECONSIDERABLE);
+            BOOST_CHECK(
+                submit_package_too_low.m_tx_results.at(tx_parent_cheap->GetId())
+                    .m_effective_feerate.value() ==
+                CFeeRate(parent_fee,
+                         GetVirtualTransactionSize(*tx_parent_cheap)));
+            // Package feerate of parent + child is too low.
+            BOOST_CHECK_EQUAL(
+                submit_package_too_low.m_tx_results.at(tx_child_cheap->GetId())
+                    .m_state.GetResult(),
+                TxValidationResult::TX_RECONSIDERABLE);
+            BOOST_CHECK(
+                submit_package_too_low.m_tx_results.at(tx_child_cheap->GetId())
+                    .m_effective_feerate.value() ==
+                CFeeRate(parent_fee + child_fee,
+                         GetVirtualTransactionSize(*tx_parent_cheap) +
+                             GetVirtualTransactionSize(*tx_child_cheap)));
+        }
         BOOST_CHECK_EQUAL(submit_package_too_low.m_state.GetResult(),
-                          PackageValidationResult::PCKG_POLICY);
+                          PackageValidationResult::PCKG_TX);
         BOOST_CHECK_EQUAL(submit_package_too_low.m_state.GetRejectReason(),
-                          "package-fee-too-low");
+                          "transaction failed");
         BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
     }
 
@@ -762,39 +777,40 @@
         const auto submit_prioritised_package = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool,
             package_still_too_low, /*test_accept=*/false);
+        if (auto err_prioritised{CheckPackageMempoolAcceptResult(
+                package_still_too_low, submit_prioritised_package,
+                /*expect_valid=*/true, m_node.mempool.get())}) {
+            BOOST_ERROR(err_prioritised.value());
+        } else {
+            const CFeeRate expected_feerate(
+                1 * COIN + parent_fee + child_fee,
+                GetVirtualTransactionSize(*tx_parent_cheap) +
+                    GetVirtualTransactionSize(*tx_child_cheap));
+            BOOST_CHECK_EQUAL(submit_prioritised_package.m_tx_results.size(),
+                              package_still_too_low.size());
+            auto it_parent = submit_prioritised_package.m_tx_results.find(
+                tx_parent_cheap->GetId());
+            auto it_child = submit_prioritised_package.m_tx_results.find(
+                tx_child_cheap->GetId());
+            BOOST_CHECK(it_parent->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_parent->second.m_base_fees.value() == parent_fee);
+            BOOST_CHECK(it_parent->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            BOOST_CHECK(it_child->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_child->second.m_base_fees.value() == child_fee);
+            BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
+                        expected_feerate);
+            std::vector<TxId> expected_txids(
+                {tx_parent_cheap->GetId(), tx_child_cheap->GetId()});
+            BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+            BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
+                        expected_txids);
+        }
         expected_pool_size += 2;
-        BOOST_CHECK_MESSAGE(
-            submit_prioritised_package.m_state.IsValid(),
-            "Package validation unexpectedly failed"
-                << submit_prioritised_package.m_state.GetRejectReason());
-        const CFeeRate expected_feerate(
-            1 * COIN + parent_fee + child_fee,
-            GetVirtualTransactionSize(*tx_parent_cheap) +
-                GetVirtualTransactionSize(*tx_child_cheap));
-        BOOST_CHECK_EQUAL(submit_prioritised_package.m_tx_results.size(),
-                          package_still_too_low.size());
-        auto it_parent = submit_prioritised_package.m_tx_results.find(
-            tx_parent_cheap->GetId());
-        auto it_child = submit_prioritised_package.m_tx_results.find(
-            tx_child_cheap->GetId());
-        BOOST_CHECK(it_parent != submit_prioritised_package.m_tx_results.end());
-        BOOST_CHECK(it_parent->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_parent->second.m_base_fees.value() == parent_fee);
-        BOOST_CHECK(it_parent->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        BOOST_CHECK(it_child != submit_prioritised_package.m_tx_results.end());
-        BOOST_CHECK(it_child->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_child->second.m_base_fees.value() == child_fee);
-        BOOST_CHECK(it_child->second.m_effective_feerate.value() ==
-                    expected_feerate);
-        std::vector<TxId> expected_txids(
-            {tx_parent_cheap->GetId(), tx_child_cheap->GetId()});
-        BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
-        BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() ==
-                    expected_txids);
+        BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
     }
 
     // Package feerate is calculated without topology in mind; it's just
@@ -828,43 +844,43 @@
         const auto submit_rich_parent = ProcessNewPackage(
             m_node.chainman->ActiveChainstate(), *m_node.mempool,
             package_rich_parent, /* test_accept */ false);
+        if (auto err_rich_parent{CheckPackageMempoolAcceptResult(
+                package_rich_parent, submit_rich_parent, /*expect_valid=*/false,
+                m_node.mempool.get())}) {
+            BOOST_ERROR(err_rich_parent.value());
+        } else {
+            // The child would have been validated on its own and failed.
+            BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetResult(),
+                              PackageValidationResult::PCKG_TX);
+            BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetRejectReason(),
+                              "transaction failed");
+
+            auto it_parent =
+                submit_rich_parent.m_tx_results.find(tx_parent_rich->GetId());
+            auto it_child =
+                submit_rich_parent.m_tx_results.find(tx_child_poor->GetId());
+            BOOST_CHECK(it_parent->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::VALID);
+            BOOST_CHECK(it_child->second.m_result_type ==
+                        MempoolAcceptResult::ResultType::INVALID);
+            BOOST_CHECK(it_parent->second.m_state.GetRejectReason() == "");
+            BOOST_CHECK_MESSAGE(
+                it_parent->second.m_base_fees.value() == high_parent_fee,
+                strprintf("rich parent: expected fee %s, got %s",
+                          high_parent_fee,
+                          it_parent->second.m_base_fees.value()));
+            BOOST_CHECK(it_parent->second.m_effective_feerate ==
+                        CFeeRate(high_parent_fee,
+                                 GetVirtualTransactionSize(*tx_parent_rich)));
+            BOOST_CHECK_EQUAL(it_child->second.m_result_type,
+                              MempoolAcceptResult::ResultType::INVALID);
+            BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(),
+                              TxValidationResult::TX_MEMPOOL_POLICY);
+            BOOST_CHECK(it_child->second.m_state.GetRejectReason() ==
+                        "min relay fee not met");
+        }
         expected_pool_size += 1;
-        BOOST_CHECK_MESSAGE(submit_rich_parent.m_state.IsInvalid(),
-                            "Package validation unexpectedly succeeded");
-
-        // The child would have been validated on its own and failed, then
-        // submitted as a "package" of 1.
-        BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetResult(),
-                          PackageValidationResult::PCKG_TX);
-        BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetRejectReason(),
-                          "transaction failed");
-
-        auto it_parent =
-            submit_rich_parent.m_tx_results.find(tx_parent_rich->GetId());
-        BOOST_CHECK(it_parent != submit_rich_parent.m_tx_results.end());
-        BOOST_CHECK(it_parent->second.m_result_type ==
-                    MempoolAcceptResult::ResultType::VALID);
-        BOOST_CHECK(it_parent->second.m_state.GetRejectReason() == "");
-        BOOST_CHECK_MESSAGE(
-            it_parent->second.m_base_fees.value() == high_parent_fee,
-            strprintf("rich parent: expected fee %s, got %s", high_parent_fee,
-                      it_parent->second.m_base_fees.value()));
-        BOOST_CHECK(it_parent->second.m_effective_feerate ==
-                    CFeeRate(high_parent_fee,
-                             GetVirtualTransactionSize(*tx_parent_rich)));
-        auto it_child =
-            submit_rich_parent.m_tx_results.find(tx_child_poor->GetId());
-        BOOST_CHECK(it_child != submit_rich_parent.m_tx_results.end());
-        BOOST_CHECK_EQUAL(it_child->second.m_result_type,
-                          MempoolAcceptResult::ResultType::INVALID);
-        BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(),
-                          TxValidationResult::TX_MEMPOOL_POLICY);
-        BOOST_CHECK(it_child->second.m_state.GetRejectReason() ==
-                    "min relay fee not met");
-
         BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-        BOOST_CHECK(m_node.mempool->exists(tx_parent_rich->GetId()));
-        BOOST_CHECK(!m_node.mempool->exists(tx_child_poor->GetId()));
     }
 }
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/util/txmempool.h b/src/test/util/txmempool.h
new file mode 100644
--- /dev/null
+++ b/src/test/util/txmempool.h
@@ -0,0 +1,23 @@
+// Copyright (c) 2022 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_TEST_UTIL_TXMEMPOOL_H
+#define BITCOIN_TEST_UTIL_TXMEMPOOL_H
+
+#include <policy/packages.h>
+#include <txmempool.h>
+
+struct PackageMempoolAcceptResult;
+
+/**
+ * Check expected properties for every PackageMempoolAcceptResult, regardless of
+ * value. Returns a string if an error occurs with error populated, nullopt
+ * otherwise. If mempool is provided, checks that the expected transactions are
+ * in mempool (this should be set to nullptr for a test_accept).
+ */
+std::optional<std::string>
+CheckPackageMempoolAcceptResult(const Package &txns,
+                                const PackageMempoolAcceptResult &result,
+                                bool expect_valid, const CTxMemPool *mempool);
+#endif // BITCOIN_TEST_UTIL_TXMEMPOOL_H
diff --git a/src/test/util/txmempool.cpp b/src/test/util/txmempool.cpp
new file mode 100644
--- /dev/null
+++ b/src/test/util/txmempool.cpp
@@ -0,0 +1,93 @@
+// Copyright (c) 2022 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <test/util/txmempool.h>
+
+#include <node/context.h>
+#include <txmempool.h>
+#include <validation.h>
+
+std::optional<std::string>
+CheckPackageMempoolAcceptResult(const Package &txns,
+                                const PackageMempoolAcceptResult &result,
+                                bool expect_valid, const CTxMemPool *mempool) {
+    if (expect_valid) {
+        if (result.m_state.IsInvalid()) {
+            return strprintf("Package validation unexpectedly failed: %s",
+                             result.m_state.ToString());
+        }
+    } else {
+        if (result.m_state.IsValid()) {
+            return strprintf("Package validation unexpectedly succeeded. %s",
+                             result.m_state.ToString());
+        }
+    }
+    if (result.m_state.GetResult() != PackageValidationResult::PCKG_POLICY &&
+        txns.size() != result.m_tx_results.size()) {
+        return strprintf("txns size %u does not match tx results size %u",
+                         txns.size(), result.m_tx_results.size());
+    }
+    for (const auto &tx : txns) {
+        const auto &txid = tx->GetId();
+        if (result.m_tx_results.count(txid) == 0) {
+            return strprintf("result not found for tx %s", txid.ToString());
+        }
+
+        const auto &atmp_result = result.m_tx_results.at(txid);
+        const bool valid{atmp_result.m_result_type ==
+                         MempoolAcceptResult::ResultType::VALID};
+        if (expect_valid && atmp_result.m_state.IsInvalid()) {
+            return strprintf("tx %s unexpectedly failed: %s", txid.ToString(),
+                             atmp_result.m_state.ToString());
+        }
+
+        // m_vsize and m_base_fees should exist iff the result was VALID or
+        // MEMPOOL_ENTRY
+        const bool mempool_entry{
+            atmp_result.m_result_type ==
+            MempoolAcceptResult::ResultType::MEMPOOL_ENTRY};
+        if (atmp_result.m_base_fees.has_value() != (valid || mempool_entry)) {
+            return strprintf("tx %s result should %shave m_base_fees",
+                             txid.ToString(),
+                             valid || mempool_entry ? "" : "not ");
+        }
+        if (atmp_result.m_vsize.has_value() != (valid || mempool_entry)) {
+            return strprintf("tx %s result should %shave m_vsize",
+                             txid.ToString(),
+                             valid || mempool_entry ? "" : "not ");
+        }
+
+        // m_effective_feerate and m_txids_fee_calculations should exist iff the
+        // result was valid or if the failure was TX_RECONSIDERABLE
+        const bool valid_or_reconsiderable{
+            atmp_result.m_result_type ==
+                MempoolAcceptResult::ResultType::VALID ||
+            atmp_result.m_state.GetResult() ==
+                TxValidationResult::TX_RECONSIDERABLE};
+        if (atmp_result.m_effective_feerate.has_value() !=
+            valid_or_reconsiderable) {
+            return strprintf("tx %s result should %shave m_effective_feerate",
+                             txid.ToString(), valid ? "" : "not ");
+        }
+        if (atmp_result.m_txids_fee_calculations.has_value() !=
+            valid_or_reconsiderable) {
+            return strprintf("tx %s result should %shave m_effective_feerate",
+                             txid.ToString(), valid ? "" : "not ");
+        }
+
+        if (mempool) {
+            // The tx by txid should be in the mempool iff the result was not
+            // INVALID.
+            const bool txid_in_mempool{
+                atmp_result.m_result_type !=
+                MempoolAcceptResult::ResultType::INVALID};
+            if (mempool->exists(tx->GetId()) != txid_in_mempool) {
+                return strprintf("tx %s should %sbe in mempool",
+                                 txid.ToString(),
+                                 txid_in_mempool ? "" : "not ");
+            }
+        }
+    }
+    return std::nullopt;
+}
diff --git a/src/validation.h b/src/validation.h
--- a/src/validation.h
+++ b/src/validation.h
@@ -179,9 +179,30 @@
 void PruneBlockFilesManual(Chainstate &active_chainstate,
                            int nManualPruneHeight);
 
+// clang-format off
 /**
- * Validation result for a single transaction mempool acceptance.
+ * Validation result for a transaction evaluated by MemPoolAccept (single or
+ * package).
+ * Here are the expected fields and properties of a result depending on its
+ * ResultType, applicable to results returned from package evaluation:
+ *+--------------------------+-----------+---------------------------------+----------------+
+ *| Field or property        |   VALID   |             INVALID             |  MEMPOOL_ENTRY |
+ *|                          |           |---------------------------------|                |
+ *|                          |           | TX_RECONSIDERABLE |    Other    |                |
+ *+--------------------------+-----------+-------------------+-------------+----------------+
+ *| txid in mempool?         | yes       | no                | no*         | yes            |
+ *| m_state                  | IsValid() | IsInvalid()       | IsInvalid() | IsValid()      |
+ *| m_replaced_transactions  | yes       | no                | no          | no             |
+ *| m_vsize                  | yes       | no                | no          | yes            |
+ *| m_base_fees              | yes       | no                | no          | yes            |
+ *| m_effective_feerate      | yes       | yes               | no          | no             |
+ *| m_txids_fee_calculations | yes       | yes               | no          | no             |
+ *+--------------------------+-----------+-------------------+-------------+----------------+
+ * (*) Individual transaction acceptance doesn't return MEMPOOL_ENTRY. It
+ * returns INVALID, with the error txn-already-in-mempool. In this case, the
+ * txid may be in the mempool for a TX_CONFLICT.
  */
+// clang-format on
 struct MempoolAcceptResult {
     /** Used to indicate the results of mempool validation. */
     enum class ResultType {
@@ -198,8 +219,6 @@
     /** Contains information about why the transaction failed. */
     const TxValidationState m_state;
 
-    // The following fields are only present when m_result_type =
-    // ResultType::VALID or MEMPOOL_ENTRY
     /**
      * Virtual size as used by the mempool, calculated using serialized size
      * and sigchecks.
@@ -212,8 +231,7 @@
      * fee delta added using prioritisetransaction (i.e. modified fees). If this
      * transaction was submitted as a package, this is the package feerate,
      * which may also include its descendants and/or ancestors
-     * (see m_txids_fee_calculations below). Only present when
-     * m_result_type = ResultType::VALID.
+     * (see m_txids_fee_calculations below).
      */
     const std::optional<CFeeRate> m_effective_feerate;
     /**
@@ -221,13 +239,20 @@
      * Includes this transaction's txid and may include others if this
      * transaction was validated as part of a package. This is not necessarily
      * equivalent to the list of transactions passed to ProcessNewPackage().
-     * Only present when m_result_type = ResultType::VALID. */
+     */
     const std::optional<std::vector<TxId>> m_txids_fee_calculations;
 
     static MempoolAcceptResult Failure(TxValidationState state) {
         return MempoolAcceptResult(state);
     }
 
+    static MempoolAcceptResult
+    FeeFailure(TxValidationState state, CFeeRate effective_feerate,
+               const std::vector<TxId> &txids_fee_calculations) {
+        return MempoolAcceptResult(state, effective_feerate,
+                                   txids_fee_calculations);
+    }
+
     /** Constructor for success case */
     static MempoolAcceptResult
     Success(int64_t vsize, Amount fees, CFeeRate effective_feerate,
@@ -264,6 +289,14 @@
           m_effective_feerate(effective_feerate),
           m_txids_fee_calculations(txids_fee_calculations) {}
 
+    /** Constructor for fee-related failure case */
+    explicit MempoolAcceptResult(
+        TxValidationState state, CFeeRate effective_feerate,
+        const std::vector<TxId> &txids_fee_calculations)
+        : m_result_type(ResultType::INVALID), m_state(state),
+          m_effective_feerate(effective_feerate),
+          m_txids_fee_calculations(txids_fee_calculations) {}
+
     /** Constructor for already-in-mempool case. */
     explicit MempoolAcceptResult(int64_t vsize, Amount fees)
         : m_result_type(ResultType::MEMPOOL_ENTRY), m_vsize{vsize},
diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -529,7 +529,7 @@
         if (mempoolRejectFee > Amount::zero() &&
             package_fee < mempoolRejectFee) {
             return state.Invalid(
-                TxValidationResult::TX_MEMPOOL_POLICY,
+                TxValidationResult::TX_RECONSIDERABLE,
                 "mempool min fee not met",
                 strprintf("%d < %d", package_fee, mempoolRejectFee));
         }
@@ -538,7 +538,7 @@
         // policy upgrade.
         if (package_fee < m_pool.m_min_relay_feerate.GetFee(package_size)) {
             return state.Invalid(
-                TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
+                TxValidationResult::TX_RECONSIDERABLE, "min relay fee not met",
                 strprintf("%d < %d", package_fee,
                           m_pool.m_min_relay_feerate.GetFee(package_size)));
         }
@@ -726,6 +726,9 @@
     if (!bypass_limits &&
         ws.m_modified_fees <
             m_pool.m_min_relay_feerate.GetFee(ws.m_ptx->GetTotalSize())) {
+        // Even though this is a fee-related failure, this result is
+        // TX_MEMPOOL_POLICY, not TX_RECONSIDERABLE, because it cannot be
+        // bypassed using package validation.
         return state.Invalid(
             TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
             strprintf("%d < %d", ws.m_modified_fees,
@@ -806,7 +809,9 @@
     if (!args.m_package_submission && !bypass_limits) {
         m_pool.LimitSize(m_active_chainstate.CoinsTip());
         if (!m_pool.exists(txid)) {
-            return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY,
+            // The tx no longer meets our (new) mempool minimum feerate but
+            // could be reconsidered in a package.
+            return state.Invalid(TxValidationResult::TX_RECONSIDERABLE,
                                  "mempool full");
         }
     }
@@ -934,10 +939,19 @@
     Workspace ws(ptx,
                  GetNextBlockScriptFlags(tip, m_active_chainstate.m_chainman));
 
+    const std::vector<TxId> single_txid{ws.m_ptx->GetId()};
+
     // Perform the inexpensive checks first and avoid hashing and signature
     // verification unless those checks pass, to mitigate CPU exhaustion
     // denial-of-service attacks.
     if (!PreChecks(args, ws)) {
+        if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
+            // Failed for fee reasons. Provide the effective feerate and which
+            // tx was included.
+            return MempoolAcceptResult::FeeFailure(
+                ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize),
+                single_txid);
+        }
         return MempoolAcceptResult::Failure(ws.m_state);
     }
 
@@ -968,7 +982,6 @@
 
     const CFeeRate effective_feerate{ws.m_modified_fees,
                                      static_cast<uint32_t>(ws.m_vsize)};
-    const std::vector<TxId> single_txid{ws.m_ptx->GetId()};
     // Tx was accepted, but not added
     if (args.m_test_accept) {
         return MempoolAcceptResult::Success(ws.m_vsize, ws.m_base_fees,
@@ -976,7 +989,12 @@
     }
 
     if (!Finalize(args, ws)) {
-        return MempoolAcceptResult::Failure(ws.m_state);
+        // The only possible failure reason is fee-related (mempool full).
+        // Failed for fee reasons. Provide the effective feerate and which txns
+        // were included.
+        Assume(ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE);
+        return MempoolAcceptResult::FeeFailure(
+            ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), single_txid);
     }
 
     GetMainSignals().TransactionAddedToMempool(
@@ -1046,20 +1064,24 @@
         workspaces.cbegin(), workspaces.cend(), Amount::zero(),
         [](Amount sum, auto &ws) { return sum + ws.m_modified_fees; });
     const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize);
-    TxValidationState placeholder_state;
-    if (args.m_package_feerates &&
-        !CheckFeeRate(m_total_size, m_total_vsize, m_total_modified_fees,
-                      placeholder_state)) {
-        package_state.Invalid(PackageValidationResult::PCKG_POLICY,
-                              "package-fee-too-low");
-        return PackageMempoolAcceptResult(package_state, {});
-    }
-
     std::vector<TxId> all_package_txids;
     all_package_txids.reserve(workspaces.size());
     std::transform(workspaces.cbegin(), workspaces.cend(),
                    std::back_inserter(all_package_txids),
                    [](const auto &ws) { return ws.m_ptx->GetId(); });
+    TxValidationState placeholder_state;
+    if (args.m_package_feerates &&
+        !CheckFeeRate(m_total_size, m_total_vsize, m_total_modified_fees,
+                      placeholder_state)) {
+        package_state.Invalid(PackageValidationResult::PCKG_TX,
+                              "transaction failed");
+        return PackageMempoolAcceptResult(
+            package_state, {{workspaces.back().m_ptx->GetId(),
+                             MempoolAcceptResult::FeeFailure(
+                                 placeholder_state,
+                                 CFeeRate(m_total_modified_fees, m_total_vsize),
+                                 all_package_txids)}});
+    }
 
     for (Workspace &ws : workspaces) {
         ws.m_package_feerate = package_feerate;
@@ -1214,7 +1236,7 @@
                 assert(m_pool.exists(txid));
                 results_final.emplace(txid, single_res);
             } else if (single_res.m_state.GetResult() !=
-                           TxValidationResult::TX_MEMPOOL_POLICY &&
+                           TxValidationResult::TX_RECONSIDERABLE &&
                        single_res.m_state.GetResult() !=
                            TxValidationResult::TX_MISSING_INPUTS) {
                 // Package validation policy only differs from individual policy