diff --git a/pallets/distance/src/lib.rs b/pallets/distance/src/lib.rs
index 0a13a751c08fc503f07c50da2dca6b399582f4f9..23277e89b1bda102ccab6ba2b94d2caac5c42d7b 100644
--- a/pallets/distance/src/lib.rs
+++ b/pallets/distance/src/lib.rs
@@ -17,7 +17,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 mod median;
-mod traits;
+pub mod traits;
 mod types;
 mod weights;
 
@@ -66,6 +66,7 @@ pub mod pallet {
         + pallet_identity::Config<IdtyIndex = IdtyIndex>
         + pallet_session::Config
     {
+        /// Currency type used in this pallet (used for reserve/slash)
         type Currency: ReservableCurrency<Self::AccountId>;
         /// Amount reserved during evaluation
         #[pallet::constant]
@@ -81,6 +82,8 @@ pub mod pallet {
         type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
         /// Type representing the weight of this pallet
         type WeightInfo: WeightInfo;
+        /// Handler for successful distance evaluation
+        type OnValidDistanceStatus: OnValidDistanceStatus<Self>;
     }
 
     // STORAGE //
@@ -139,20 +142,6 @@ pub mod pallet {
         OptionQuery,
     >;
 
-    /// Identities by distance status expiration session index
-    #[pallet::storage]
-    #[pallet::getter(fn distance_status_expire_on)]
-    pub type DistanceStatusExpireOn<T: Config> = StorageMap<
-        _,
-        Twox64Concat,
-        u32,
-        BoundedVec<
-            <T as pallet_identity::Config>::IdtyIndex,
-            ConstU32<MAX_EVALUATIONS_PER_SESSION>,
-        >,
-        ValueQuery,
-    >;
-
     /// Did evaluation get updated in this block?
     #[pallet::storage]
     pub(super) type DidUpdate<T: Config> = StorageValue<_, bool, ValueQuery>;
@@ -224,7 +213,9 @@ pub mod pallet {
 
     #[pallet::call]
     impl<T: Config> Pallet<T> {
-        /// Request an identity to be evaluated
+        /// Request caller identity to be evaluated
+        /// positive evaluation will result in claim/renew membership
+        /// negative evaluation will result in slash for caller
         #[pallet::call_index(0)]
         #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation())]
         pub fn request_distance_evaluation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
@@ -233,6 +224,8 @@ pub mod pallet {
             let idty =
                 pallet_identity::IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?;
 
+            // TODO is it necessary to check that the same account performed the request?
+            // TODO what if the distance status is existing but valid?
             ensure!(
                 IdentityDistanceStatus::<T>::get(idty)
                     != Some((who.clone(), DistanceStatus::Pending)),
@@ -243,13 +236,49 @@ pub mod pallet {
             Ok(().into())
         }
 
+        /// Request target identity to be evaluated
+        /// only possible for unconfirmed identity
+        #[pallet::call_index(4)]
+        #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation())]
+        pub fn request_distance_evaluation_for(
+            origin: OriginFor<T>,
+            target: T::IdtyIndex,
+        ) -> DispatchResultWithPostInfo {
+            let who = ensure_signed(origin)?;
+
+            // check that the caller has an identity (TODO is this necessary ?)
+            // let _ =
+            //     pallet_identity::IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?;
+
+            // get the identity value of the target
+            let target_idty =
+                pallet_identity::Identities::<T>::get(target).ok_or(Error::<T>::NoIdentity)?;
+
+            // check that target is unconfirmed
+            ensure!(
+                target_idty.status == pallet_identity::IdtyStatus::Unconfirmed,
+                Error::<T>::NoIdentity
+            );
+
+            // check that no distance status is already there
+            ensure!(
+                IdentityDistanceStatus::<T>::get(target).is_none(),
+                Error::<T>::AlreadyInEvaluation
+            );
+
+            Pallet::<T>::do_request_distance_evaluation(who, target)?;
+            Ok(().into())
+        }
+
         /// (Inherent) Push an evaluation result to the pool
+        /// this is called internally by validators (= inherent)
         #[pallet::call_index(1)]
         #[pallet::weight(<T as pallet::Config>::WeightInfo::update_evaluation(MAX_EVALUATIONS_PER_SESSION))]
         pub fn update_evaluation(
             origin: OriginFor<T>,
             computation_result: ComputationResult,
         ) -> DispatchResult {
+            // no origin = inherent
             ensure_none(origin)?;
             ensure!(
                 !DidUpdate::<T>::exists(),
@@ -263,7 +292,8 @@ pub mod pallet {
             Ok(())
         }
 
-        /// Push an evaluation result to the pool
+        /// Force push an evaluation result to the pool
+        // (it is convenient to have this call in end2end tests)
         #[pallet::call_index(2)]
         #[pallet::weight(<T as pallet::Config>::WeightInfo::force_update_evaluation(MAX_EVALUATIONS_PER_SESSION))]
         pub fn force_update_evaluation(
@@ -276,7 +306,8 @@ pub mod pallet {
             Pallet::<T>::do_update_evaluation(evaluator, computation_result)
         }
 
-        /// Set the distance evaluation status of an identity
+        /// Force set the distance evaluation status of an identity
+        // (it is convenient to have this in test network)
         ///
         /// Removes the status if `status` is `None`.
         ///
@@ -292,15 +323,7 @@ pub mod pallet {
         ) -> DispatchResult {
             ensure_root(origin)?;
 
-            IdentityDistanceStatus::<T>::set(identity, status.clone());
-            DistanceStatusExpireOn::<T>::mutate(
-                pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(),
-                move |identities| {
-                    identities
-                        .try_push(identity)
-                        .map_err(|_| Error::<T>::TooManyEvaluationsInBlock)
-                },
-            )?;
+            Self::do_set_distance_status(identity, status.clone());
             Self::deposit_event(Event::EvaluationStatusForced {
                 idty_index: identity,
                 status,
@@ -309,27 +332,6 @@ pub mod pallet {
         }
     }
 
-    // BENCHMARK FUNCTIONS //
-
-    impl<T: Config> Pallet<T> {
-        /// Force the distance status using IdtyIndex and AccountId
-        /// only to prepare identity for benchmarking.
-        pub fn set_distance_status(
-            identity: <T as pallet_identity::Config>::IdtyIndex,
-            status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>,
-        ) -> DispatchResult {
-            IdentityDistanceStatus::<T>::set(identity, status);
-            DistanceStatusExpireOn::<T>::mutate(
-                pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(),
-                move |identities| {
-                    identities
-                        .try_push(identity)
-                        .map_err(|_| Error::<T>::TooManyEvaluationsInBlock.into())
-                },
-            )
-        }
-    }
-
     // INTERNAL FUNCTIONS //
 
     impl<T: Config> Pallet<T> {
@@ -394,6 +396,7 @@ pub mod pallet {
             }
         }
 
+        /// request distance evaluation in current pool
         fn do_request_distance_evaluation(
             who: T::AccountId,
             idty_index: <T as pallet_identity::Config>::IdtyIndex,
@@ -418,31 +421,31 @@ pub mod pallet {
                         (&who, DistanceStatus::Pending),
                     );
 
-                    DistanceStatusExpireOn::<T>::mutate(
-                        pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(),
-                        move |identities| identities.try_push(idty_index).ok(),
-                    );
                     Self::deposit_event(Event::EvaluationRequested { idty_index, who });
                     Ok(())
                 },
             )
         }
 
+        /// update distance evaluation in next pool
         fn do_update_evaluation(
             evaluator: <T as frame_system::Config>::AccountId,
             computation_result: ComputationResult,
         ) -> DispatchResult {
             Pallet::<T>::mutate_next_pool(pallet_session::CurrentIndex::<T>::get(), |result_pool| {
+                // evaluation must be provided for all identities (no more, no less)
                 ensure!(
                     computation_result.distances.len() == result_pool.evaluations.len(),
                     Error::<T>::WrongResultLength
                 );
 
+                // insert the evaluator if not already there
                 if result_pool
                     .evaluators
                     .try_insert(evaluator.clone())
                     .map_err(|_| Error::<T>::TooManyEvaluators)?
                 {
+                    // update the median accumulator with the new result
                     for (distance_value, (_identity, median_acc)) in computation_result
                         .distances
                         .into_iter()
@@ -454,19 +457,28 @@ pub mod pallet {
                     Self::deposit_event(Event::EvaluationUpdated { evaluator });
                     Ok(())
                 } else {
+                    // one author can only submit one evaluation
                     Err(Error::<T>::TooManyEvaluationsByAuthor.into())
                 }
             })
         }
+
+        /// Set the distance status using IdtyIndex and AccountId
+        pub fn do_set_distance_status(
+            identity: <T as pallet_identity::Config>::IdtyIndex,
+            status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>,
+        ) {
+            IdentityDistanceStatus::<T>::set(identity, status.clone());
+            if let Some((_, DistanceStatus::Valid)) = status {
+                T::OnValidDistanceStatus::on_valid_distance_status(identity);
+            }
+        }
     }
 
     impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> {
         fn on_new_session(index: SessionIndex) {
             EvaluationBlock::<T>::set(frame_system::Pallet::<T>::parent_hash());
 
-            // Make results expire
-            DistanceStatusExpireOn::<T>::remove(index);
-
             // Apply the results from the current pool (which was previous session's result pool)
             // We take the results so the pool is left empty for the new session.
             #[allow(clippy::type_complexity)]
diff --git a/pallets/distance/src/mock.rs b/pallets/distance/src/mock.rs
index a05d58577b4ff3e72e8cfa9f150222e6518793cc..4c227159d72d5935659bd007fd190763c060a8ee 100644
--- a/pallets/distance/src/mock.rs
+++ b/pallets/distance/src/mock.rs
@@ -257,6 +257,7 @@ impl pallet_distance::Config for Test {
     type ResultExpiration = frame_support::traits::ConstU32<720>;
     type RuntimeEvent = RuntimeEvent;
     type WeightInfo = ();
+    type OnValidDistanceStatus = ();
 }
 
 // Build genesis storage according to the mock runtime.
diff --git a/pallets/distance/src/traits.rs b/pallets/distance/src/traits.rs
index de9d532d64f4ce64a16fd20fab28455523f18e69..eafb5451c1136bb3a1709d5370a88211fdf07fdb 100644
--- a/pallets/distance/src/traits.rs
+++ b/pallets/distance/src/traits.rs
@@ -14,8 +14,13 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
 
-pub trait HandleNegativeEvaluation<T: crate::Config> {
-    /// Do something with the reserved amount on the account,
-    /// when distance evaluation result is negative.
-    fn handle_negative_evaluation(account_id: T::AccountId);
+use crate::*;
+
+pub trait OnValidDistanceStatus<T: Config> {
+    /// Handler for valid distance evaluation
+    fn on_valid_distance_status(idty_index: T::IdtyIndex);
+}
+
+impl<T: Config> OnValidDistanceStatus<T> for () {
+    fn on_valid_distance_status(_idty_index: T::IdtyIndex) {}
 }
diff --git a/pallets/duniter-wot/src/lib.rs b/pallets/duniter-wot/src/lib.rs
index 41ad8db138320591f987b6a4c00870164d03bd62..04a02e808c373dc24b1c407abac3dc15da46297a 100644
--- a/pallets/duniter-wot/src/lib.rs
+++ b/pallets/duniter-wot/src/lib.rs
@@ -356,3 +356,28 @@ impl<T: Config<I>, I: 'static> pallet_certification::traits::OnRemovedCert<IdtyI
         }
     }
 }
+
+/// valid distance status handler
+impl<T: Config<I> + pallet_distance::Config, I: 'static>
+    pallet_distance::traits::OnValidDistanceStatus<T> for Pallet<T, I>
+{
+    fn on_valid_distance_status(idty_index: IdtyIndex) {
+        if let Some(identity) = pallet_identity::Identities::<T>::get(idty_index) {
+            match identity.status {
+                IdtyStatus::Unconfirmed => { /* should not happen */ }
+                IdtyStatus::Unvalidated | IdtyStatus::NotMember => {
+                    // ok to fail
+                    let _ = pallet_membership::Pallet::<T, I>::try_claim_membership(idty_index);
+                }
+                IdtyStatus::Member => {
+                    // ok to fail
+                    let _ = pallet_membership::Pallet::<T, I>::try_renew_membership(idty_index);
+                }
+                IdtyStatus::Revoked => { /* should not happen */ }
+            }
+        } else {
+            // identity was removed before distance status was found
+            // so it's ok to do nothing
+        }
+    }
+}
diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs
index 08bbb81ad94295f46691fde78233a633b42ad687..435d6b42d06e5db9d4134131d79d83b184d71ee4 100644
--- a/pallets/membership/src/lib.rs
+++ b/pallets/membership/src/lib.rs
@@ -203,8 +203,7 @@ pub mod pallet {
             // get identity
             let idty_id = Self::get_idty_id(origin)?;
 
-            Self::check_allowed_to_claim(idty_id)?;
-            Self::do_add_membership(idty_id);
+            Self::try_claim_membership(idty_id)?;
             Ok(().into())
         }
 
@@ -214,16 +213,8 @@ pub mod pallet {
         pub fn renew_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
             // Verify phase
             let idty_id = Self::get_idty_id(origin)?;
-            let membership_data =
-                Membership::<T, I>::get(idty_id).ok_or(Error::<T, I>::MembershipNotFound)?;
-
-            T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?;
-
-            // apply phase
-            Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
-            Self::insert_membership_and_schedule_expiry(idty_id);
-            T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id));
 
+            Self::try_renew_membership(idty_id)?;
             Ok(().into())
         }
 
@@ -274,12 +265,48 @@ pub mod pallet {
             Ok(())
         }
 
+        /// check that membership can be renewed
+        pub fn check_allowed_to_renew(
+            idty_id: T::IdtyId,
+        ) -> Result<MembershipData<T::BlockNumber>, DispatchError> {
+            let membership_data =
+                Membership::<T, I>::get(idty_id).ok_or(Error::<T, I>::MembershipNotFound)?;
+
+            // enough certifications and distance rule for example
+            T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?;
+            Ok(membership_data)
+        }
+
+        /// try claim membership
+        pub fn try_claim_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
+            Self::check_allowed_to_claim(idty_id)?;
+            Self::do_add_membership(idty_id);
+            Ok(())
+        }
+
+        /// try renew membership
+        pub fn try_renew_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
+            let membership_data = Self::check_allowed_to_renew(idty_id)?;
+            Self::do_renew_membership(idty_id, membership_data);
+            Ok(())
+        }
+
         /// perform membership addition
         fn do_add_membership(idty_id: T::IdtyId) {
             Self::insert_membership_and_schedule_expiry(idty_id);
             T::OnEvent::on_event(&sp_membership::Event::MembershipAdded(idty_id));
         }
 
+        /// perform membership renewal
+        fn do_renew_membership(
+            idty_id: T::IdtyId,
+            membership_data: MembershipData<T::BlockNumber>,
+        ) {
+            Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
+            Self::insert_membership_and_schedule_expiry(idty_id);
+            T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id));
+        }
+
         /// perform membership removal
         pub fn do_remove_membership(idty_id: T::IdtyId, reason: MembershipRemovalReason) {
             if let Some(membership_data) = Membership::<T, I>::take(idty_id) {
diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs
index a9eedceef109638c2d9ec9ecb73807990abcd15e..9de85321c0291f15f7063cc79fde44a34980d5ae 100644
--- a/runtime/common/src/pallets_config.rs
+++ b/runtime/common/src/pallets_config.rs
@@ -522,6 +522,7 @@ macro_rules! pallets_config {
             type ResultExpiration = frame_support::traits::ConstU32<720>;
             type RuntimeEvent = RuntimeEvent;
             type WeightInfo = common_runtime::weights::pallet_distance::WeightInfo<Runtime>;
+            type OnValidDistanceStatus = Wot;
         }
 
         // SMITHS SUB-WOT //