diff --git a/Phys/TrackRefitting/CMakeLists.txt b/Phys/TrackRefitting/CMakeLists.txt
index 09c8cb554d9ef8905de7cc787c5988763dd8a290..93073694378ef5963818f271efb123c4b969c7ba 100644
--- a/Phys/TrackRefitting/CMakeLists.txt
+++ b/Phys/TrackRefitting/CMakeLists.txt
@@ -20,6 +20,7 @@ gaudi_add_module(TrackRefitting
         src/SelectTracksForParticles.cpp
         src/SelectTracksForRecVertices.cpp
         src/UpdateVertexCoordinatesOffline.cpp
+        src/TrackScaleState.cpp
     LINK
         Gaudi::GaudiAlgLib
         Gaudi::GaudiKernel
diff --git a/Phys/TrackRefitting/src/TrackScaleState.cpp b/Phys/TrackRefitting/src/TrackScaleState.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cec40c85fb98ef2d7be72c5eb9c7f27b791b77e3
--- /dev/null
+++ b/Phys/TrackRefitting/src/TrackScaleState.cpp
@@ -0,0 +1,215 @@
+/*****************************************************************************\
+* (c) Copyright 2025 CERN for the benefit of the LHCb Collaboration           *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+
+/**
+ * @class TrackScaleState
+ *
+ * Uses a table of corrections (from the conditions) to update
+ * the states on tracks, per the method presented in LHCb-DP-2023-003.
+ *
+ * As the objects on the TES are immutable, the results of
+ * this algorithm can be described as:
+ * (1) tracks;
+ * (2) a relation table
+ *
+ * The intended use-case for this is in DaVinci, in which
+ * an offline calibration can be applied to the momenta
+ * of the particles.
+ *
+ * By default, when provided with VELO tracks, nothing changes.
+ * All states on the track are adjusted.
+ *
+ * @author Ozlem Ozcelik <ozlem.ozcelik@cern.ch>
+ * @author Matthew David Needham <Matthew.Needham@cern.ch>
+ * @author Laurent Dufour <laurent.dufour@cern.ch> (only technical aspects)
+ */
+#include "Event/Track.h"
+#include "GaudiKernel/ISvcLocator.h"
+
+#include "LHCbAlgs/Transformer.h"
+#include "Relations/Relation2D.h"
+
+#include "DetDesc/DetectorElement.h"
+#include "DetDesc/GenericConditionAccessorHolder.h"
+
+#include "Kernel/STLExtensions.h"
+
+#include <Event/ODIN.h>
+
+#ifdef USE_DD4HEP
+#  include <Detector/LHCb/DeLHCb.h>
+#  include <Detector/LHCb/OfflineMomentumScale.h>
+
+namespace {
+  using OutputRelationTable = LHCb::Relation2D<LHCb::Track, LHCb::Track>;
+  using Output_t            = std::tuple<LHCb::Tracks, OutputRelationTable>;
+
+  using MultiTransformer_t = LHCb::Algorithm::MultiTransformer<Output_t( LHCb::Track::Range const&, LHCb::ODIN const&,
+                                                                         LHCb::Detector::DeLHCb const& ),
+                                                               LHCb::DetDesc::usesConditions<LHCb::Detector::DeLHCb>>;
+
+  inline float value_within_range( float value, std::set<float> const& bins ) {
+    if ( value < *bins.begin() ) return *bins.begin();
+
+    if ( value > *bins.rbegin() ) return *bins.rbegin();
+
+    return value;
+  }
+} // namespace
+
+namespace LHCb {
+  class TrackScaleState final : public MultiTransformer_t {
+
+  public:
+    TrackScaleState( const std::string& name, ISvcLocator* pSvcLocator )
+        : MultiTransformer_t( name, pSvcLocator,
+                              { KeyValue{ "InputTracks", "" }, KeyValue{ "ODIN", "" },
+                                KeyValue{ "StandardGeometryTop", LHCb::standard_geometry_top } },
+                              {
+                                  KeyValue{ "OutputTracks", "" },
+                                  KeyValue{ "OutputRelations", "" },
+                              } ) {}
+
+    Output_t operator()( LHCb::Track::Range const& inputTracks, LHCb::ODIN const& odin,
+                         LHCb::Detector::DeLHCb const& lhcb ) const override {
+
+      const auto& momentumScaleCalibration = lhcb.offlineMomentumScale();
+
+      Output_t myOutput{};
+
+      auto& outputTracks    = std::get<0>( myOutput );
+      auto& outputRelations = std::get<1>( myOutput );
+
+      if ( !momentumScaleCalibration || !momentumScaleCalibration->enabled() ) {
+        std::for_each( inputTracks.begin(), inputTracks.end(), [&outputTracks, &outputRelations]( const auto& track ) {
+          const auto& newTrack = new LHCb::Track( *track, track->key() );
+
+          outputTracks.insert( newTrack, track->key() );
+          outputRelations.relate( newTrack, track ).ignore();
+        } );
+
+        m_nSkippedTracks += inputTracks.size();
+
+        return myOutput;
+      }
+
+      outputTracks.reserve( inputTracks.size() );
+      const auto runnumber = odin.runNumber();
+
+      for ( const auto& track : inputTracks ) {
+        auto newTrack = std::make_unique<Track>( *track, track->key() );
+
+        if ( !( track->isVeloBackward() || track->checkType( LHCb::Track::Types::Velo ) ) ) {
+
+          // actually scale the states
+          for ( auto& pState : newTrack->states() ) {
+            StatusCode stateUpdated = updateState( *pState, *track, *momentumScaleCalibration, runnumber );
+            if ( !stateUpdated.isSuccess() ) ++m_msg_track_state_update_fail;
+          }
+
+          ++m_nScaledTracks;
+        } else {
+          ++m_nSkippedTracks;
+        }
+
+        auto* newTrackPtr = newTrack.release();
+        outputTracks.insert( newTrackPtr ); // once we put the pointer on the TES, memory is done by Gaudi
+        StatusCode relationResult = outputRelations.relate( newTrackPtr, track );
+
+        if ( !relationResult.isSuccess() ) { ++m_msg_relation_add_fail; }
+      }
+
+      return myOutput;
+    }
+
+  private:
+    StatusCode updateState( LHCb::State& state, LHCb::Track const& track,
+                            LHCb::Detector::OfflineMomentumScale const& momentumScale,
+                            const unsigned int                          runnumber ) const {
+      // Determine the scaling factor based on momentum
+      float scaleFactor    = 1.0;
+      bool  highMomentum   = track.p() > momentumScale.highMomentumThreshold();
+      bool  positiveCharge = track.charge() > 0;
+      float tx             = track.firstState().tx();
+      float ty             = track.firstState().ty();
+      if ( this->msgLevel( MSG::DEBUG ) ) { this->debug() << "State (tx,ty): " << tx << ", " << ty << endmsg; }
+
+      if ( m_allowOverflowUnderflow.value() ) {
+        // force the value of tx to be in the range.
+        tx = value_within_range( tx, momentumScale.binsTx() );
+        ty = value_within_range( ty, momentumScale.binsTy() );
+
+        // take care of the float comparison
+        if ( fabs( tx - track.firstState().tx() ) > 1.e-5 || fabs( ty - track.firstState().ty() ) > 1.e-5 ) {
+          ++m_nOverflowUnderflow;
+          if ( this->msgLevel( MSG::DEBUG ) ) {
+            this->debug() << "(tx,ty) after under/overflow protection: " << tx << ", " << ty << endmsg;
+          }
+          if ( this->msgLevel( MSG::DEBUG ) ) {
+            this->debug() << "Difference: " << ( tx - track.firstState().tx() ) << ", " << ty - track.firstState().ty()
+                          << endmsg;
+          }
+        }
+      }
+
+      const auto& scaleMap = momentumScale.correctionMap( highMomentum, positiveCharge );
+
+      const auto index = momentumScale.getSequentialIndex( tx, ty );
+
+      if ( !index ) {
+        if ( this->msgLevel( MSG::DEBUG ) ) { this->debug() << "(tx,ty) out of bounds: " << tx << "," << ty << endmsg; }
+
+        ++m_msg_out_of_bounds;
+        return StatusCode::FAILURE;
+      }
+
+      scaleFactor = 1. - ( ( scaleMap[*index] / 1000.0 ) );
+
+      // Apply additional scale factor based on the run number
+      auto alpha = momentumScale.getAlpha( runnumber );
+      if ( !alpha )
+        ++m_msg_no_alpha;
+      else { scaleFactor *= 1. - ( *alpha ) / 1000.0; }
+
+      // std::cout << scaleFactor << std::endl;
+
+      // Scale the momentum
+      state.setQOverP( ( 1. / scaleFactor ) * state.qOverP() );
+
+      if ( this->msgLevel( MSG::DEBUG ) ) { this->debug() << "Scalefactor applied: " << scaleFactor << endmsg; }
+
+      return StatusCode::SUCCESS;
+    }
+
+    mutable Gaudi::Accumulators::Counter<> m_nScaledTracks{ this, "# scaled tracks" };
+    mutable Gaudi::Accumulators::Counter<> m_nSkippedTracks{ this,
+                                                             "# skipped tracks (no momentum scale numbers enabled)" };
+    mutable Gaudi::Accumulators::Counter<> m_nSkippedVeloTracks{ this, "# skipped tracks (VELO)" };
+    mutable Gaudi::Accumulators::Counter<> m_nOverflowUnderflow{
+        this, "# used under/overflow bins in (tx,ty) of momentum scaling" };
+
+    mutable Gaudi::Accumulators::MsgCounter<MSG::ERROR> m_msg_relation_add_fail{ this,
+                                                                                 "Adding of relationship failed" };
+
+    mutable Gaudi::Accumulators::MsgCounter<MSG::WARNING> m_msg_no_alpha{ this, "Fill-dependent scale factor absent." };
+
+    mutable Gaudi::Accumulators::MsgCounter<MSG::ERROR> m_msg_track_state_update_fail{ this,
+                                                                                       "Failed to update track state" };
+
+    mutable Gaudi::Accumulators::MsgCounter<MSG::ERROR> m_msg_out_of_bounds{
+        this, "(tx, ty) are out of bounds for this calibration map" };
+
+    Gaudi::Property<bool> m_allowOverflowUnderflow{ this, "AllowUnderOverflow", true };
+  };
+
+  DECLARE_COMPONENT_WITH_ID( TrackScaleState, "TrackScaleState" )
+} // namespace LHCb
+#endif
diff --git a/Tr/PrKalmanFilter/src/KalmanFilterTool.cpp b/Tr/PrKalmanFilter/src/KalmanFilterTool.cpp
index e8e41b541228336b34c2edf893a9a69905052df8..46e95fdb9cb4d4da3dfee40a09677d2cae649ce7 100644
--- a/Tr/PrKalmanFilter/src/KalmanFilterTool.cpp
+++ b/Tr/PrKalmanFilter/src/KalmanFilterTool.cpp
@@ -269,6 +269,7 @@ namespace LHCb::Pr {
       auto down_chi2     = LHCb::ChiSquare{ 0, -n_track_parameters };
       auto upstream_chi2 = LHCb::ChiSquare{}; // not initialized because always used combined with one of the above
       auto muon_chi2     = LHCb::ChiSquare{};
+      unsigned int n_ut_outliers = 0;
 
       for ( auto const& node : fitnodes ) {
         switch ( node.type() ) {
@@ -277,6 +278,7 @@ namespace LHCb::Pr {
           break;
         case Node::Type::UTHit:
           upstream_chi2 += node.delta_chi2[Node::backward];
+          if ( node.m_is_outlier ) ++n_ut_outliers;
           break;
         case Node::Type::FTHit:
           down_chi2 += node.delta_chi2[Node::forward];
@@ -296,6 +298,7 @@ namespace LHCb::Pr {
         new_track.addInfo( TrackV1::AdditionalInfo::FitTNDoF, down_chi2.nDoF() );
         new_track.addInfo( TrackV1::AdditionalInfo::FitVeloChi2, velo_chi2.chi2() );
         new_track.addInfo( TrackV1::AdditionalInfo::FitVeloNDoF, velo_chi2.nDoF() );
+        new_track.addInfo( TrackV1::AdditionalInfo::NUTOutliers, n_ut_outliers );
         new_track.addInfo( TrackV1::AdditionalInfo::FitMatchChi2,
                            prev_chi2.chi2() - upstream_chi2.chi2() - down_chi2.chi2() );
         if ( add_fitted_states<Long::Tracks>( new_track, fitnodes, scatteringMomentum, geo, extrap,
@@ -311,6 +314,7 @@ namespace LHCb::Pr {
         new_track.addInfo( TrackV1::AdditionalInfo::FitVeloChi2, velo_chi2.chi2() );
         new_track.addInfo( TrackV1::AdditionalInfo::FitVeloNDoF, velo_chi2.nDoF() );
         new_track.addInfo( TrackV1::AdditionalInfo::FitMuonChi2, muon_chi2.chi2() );
+        new_track.addInfo( TrackV1::AdditionalInfo::NUTOutliers, n_ut_outliers );
         new_track.addInfo( TrackV1::AdditionalInfo::FitMatchChi2,
                            prev_chi2.chi2() - upstream_chi2.chi2() - down_chi2.chi2() - muon_chi2.chi2() );
         if ( add_fitted_states<Long::Tracks>( new_track, fitnodes, scatteringMomentum, geo, extrap,