diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index f7e03a8eede7ebc43055a16f1219ca2a113096fc..ee9040be2fb508c78b0fece2e4c865d0a098a47f 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -93,6 +93,7 @@ set(SRCS
   detectors/tof/ReadoutConfig.cxx
   detectors/tof/Unpack.cxx
   detectors/tof/Hitfind.cxx
+  detectors/tof/HitfinderChain.cxx
   detectors/bmon/ReadoutConfig.cxx
   detectors/bmon/Unpack.cxx
   detectors/trd/ReadoutConfig.cxx
diff --git a/algo/base/PartitionedSpan.h b/algo/base/PartitionedSpan.h
index 988cc6f2daba2dedbb37730f7951cffdd23af62f..e004f92b485f91e09092e3b9aebc439d43413005 100644
--- a/algo/base/PartitionedSpan.h
+++ b/algo/base/PartitionedSpan.h
@@ -101,6 +101,14 @@ namespace cbm::algo
       EnsureDimensions();
     }
 
+    template<typename U, typename = detail::EnableOnConst<U, T>>
+    PartitionedSpan(PartitionedSpan<U> other)
+      : fData(other.Data())
+      , fOffsets(other.Offsets())
+      , fAdresses(other.Addresses())
+    {
+    }
+
     gsl::span<T> operator[](size_t i) const
     {
       EnsureBounds(i);
diff --git a/algo/base/config/Yaml.h b/algo/base/config/Yaml.h
index 50e6684eb05b54ad3be7f2a48708a88f05747fc6..dbb60801885e1110366278a343cd1ab9711042a2 100644
--- a/algo/base/config/Yaml.h
+++ b/algo/base/config/Yaml.h
@@ -13,11 +13,15 @@
 #include "BaseTypes.h"
 #include "Definitions.h"
 #include "Property.h"
+#include "compat/Filesystem.h"
 #include "util/EnumDict.h"
 
 namespace cbm::algo::config
 {
 
+  template<typename T>
+  T Read(const YAML::Node& node);
+
   template<typename T, T... Values, typename Func>
   constexpr void ForEach(std::integer_sequence<T, Values...>, Func&& func)
   {
@@ -34,6 +38,13 @@ namespace cbm::algo::config
     static constexpr std::optional<YAML::EMITTER_MANIP> value = T::FormatAs;
   };
 
+  template<typename T>
+  T ReadFromFile(fs::path path)
+  {
+    YAML::Node node = YAML::LoadFile(path.string());
+    return Read<T>(node);
+  }
+
   template<typename T>
   T Read(const YAML::Node& node)
   {
diff --git a/algo/ca/TrackingChain.cxx b/algo/ca/TrackingChain.cxx
index 05d1f1d0cd909eb948ad3d8157913964c03d2bcd..39f3b7a7af2a235970eb3307d08e7eeac7488d80 100644
--- a/algo/ca/TrackingChain.cxx
+++ b/algo/ca/TrackingChain.cxx
@@ -16,7 +16,8 @@
 #include "CaInitManager.h"
 #include "CaParameters.h"
 
-using cbm::algo::PartitionedPODVector;
+using namespace cbm::algo;
+
 using cbm::algo::TrackingChain;
 using cbm::algo::ca::EDetectorID;
 using cbm::algo::ca::Framework;
@@ -32,7 +33,7 @@ using cbm::algo::ca::constants::clrs::GNb;  // grin bald text
 void TrackingChain::Init()
 {
   // ------ Read parameters from binary
-  std::string paramFileBase = "mcbm_beam_2022_05_23_nickel_w_time.ca.par";  // TODO: Get the setup name from Opts()
+  std::string paramFileBase = "mcbm_beam_2022_05_23_nickel.ca.par";  // TODO: Get the setup name from Opts()
   auto paramFile            = Opts().ParamsDir();
   paramFile /= paramFileBase;
   L_(info) << "Tracking Chain: reading CA parameters file " << GNb << paramFile.string() << CL << '\n';
@@ -48,7 +49,7 @@ void TrackingChain::Init()
 
 // ---------------------------------------------------------------------------------------------------------------------
 //
-TrackingChain::Return_t TrackingChain::Run(const RecoResults& recoResults)
+TrackingChain::Return_t TrackingChain::Run(Input_t recoResults)
 {
   // ----- Init input data ---------------------------------------------------------------------------------------------
   this->PrepareInput(recoResults);
@@ -71,13 +72,14 @@ void TrackingChain::Finalize() {}
 
 // ---------------------------------------------------------------------------------------------------------------------
 //
-void TrackingChain::PrepareInput(const RecoResults& recoResults)
+void TrackingChain::PrepareInput(Input_t recoResults)
 {
   fNofHitKeys  = 0;
-  int nHitsTot = recoResults.stsHits.NElements();
+  int nHitsTot = recoResults.stsHits.NElements() + recoResults.tofHits.NElements();
   L_(info) << "Tracking chain: input has " << nHitsTot << " hits";
   fCaDataManager.ResetInputData(nHitsTot);
   ReadHits<EDetectorID::Sts>(recoResults.stsHits);
+  ReadHits<EDetectorID::Tof>(recoResults.tofHits);
   fCaDataManager.SetNhitKeys(fNofHitKeys);
   L_(info) << "Tracking chain:" << fCaDataManager.GetNofHits() << " will be passed to the ca::Framework";
   fCaFramework.ReceiveInputData(fCaDataManager.TakeInputData());
@@ -86,8 +88,10 @@ void TrackingChain::PrepareInput(const RecoResults& recoResults)
 // ---------------------------------------------------------------------------------------------------------------------
 //
 template<EDetectorID DetID>
-void TrackingChain::ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID>>& hits)
+void TrackingChain::ReadHits(PartitionedSpan<const ca::HitTypes_t::at<DetID>> hits)
 {
+  constexpr bool IsSTS = (DetID == EDetectorID::Sts);
+
   ca::HitKeyIndex_t firstHitKey = fNofHitKeys;
   int64_t dataStreamDet         = static_cast<int64_t>(DetID) << 60;  // detector part of the data stream
   for (size_t iPartition = 0; iPartition < hits.NPartitions(); ++iPartition) {
@@ -97,7 +101,7 @@ void TrackingChain::ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID
     int iStLocal       = -1;
     // FIXME: This definition of the station index works only for STS, and there is no any guaranty, that it will
     //        work for other mCBM setups.
-    if constexpr (DetID == EDetectorID::Sts) { iStLocal = (extHitAddress >> 4) & 0xF; }
+    if constexpr (IsSTS) { iStLocal = (extHitAddress >> 4) & 0xF; }
     int iStActive  = (iStLocal != -1) ? fCaFramework.GetParameters().GetStationIndexActive(iStLocal, DetID) : -1;
     size_t iOffset = hits.Offsets()[iPartition];
     if (iStActive < 0) { continue; }
@@ -107,7 +111,7 @@ void TrackingChain::ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID
       int iHitExt     = iOffset + iPartHit;
       // ---- Fill ca::Hit
       ca::Hit caHit;
-      if constexpr (DetID == EDetectorID::Sts) {
+      if constexpr (IsSTS) {
         caHit.SetFrontKey(firstHitKey + hit.fFrontClusterId);
         caHit.SetBackKey(firstHitKey + hit.fBackClusterId);
       }
@@ -115,18 +119,18 @@ void TrackingChain::ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID
         caHit.SetFrontKey(firstHitKey + iHitExt);
         caHit.SetBackKey(caHit.FrontKey());
       }
-      caHit.SetX(hit.fX);
-      caHit.SetY(hit.fY);
-      caHit.SetZ(hit.fZ);
-      caHit.SetT(hit.fTime);
-      caHit.SetDx2(hit.fDx * hit.fDx);
-      caHit.SetDy2(hit.fDy * hit.fDy);
-      caHit.SetDxy(hit.fDxy);
-      caHit.SetDt2(hit.fTimeError * hit.fTimeError);
+      caHit.SetX(hit.X());
+      caHit.SetY(hit.Y());
+      caHit.SetZ(hit.Z());
+      caHit.SetT(hit.Time());
+      caHit.SetDx2(hit.Dx() * hit.Dx());
+      caHit.SetDy2(hit.Dy() * hit.Dy());
+      if constexpr (IsSTS) caHit.SetDxy(hit.fDxy);
+      caHit.SetDt2(hit.TimeError() * hit.TimeError());
       /// FIXME: Define ranges from the hit, when will be available
-      caHit.SetRangeX(3.5 * hit.fDx);
-      caHit.SetRangeY(3.5 * hit.fDy);
-      caHit.SetRangeT(3.5 * hit.fTimeError);
+      caHit.SetRangeX(3.5 * hit.Dx());
+      caHit.SetRangeY(3.5 * hit.Dy());
+      caHit.SetRangeT(3.5 * hit.TimeError());
       caHit.SetStation(iStActive);
       caHit.SetId(fCaDataManager.GetNofHits());
       fCaDataManager.PushBackHit(caHit, dataStream);
@@ -137,4 +141,4 @@ void TrackingChain::ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID
   }    // iPartition
 }
 
-template void TrackingChain::ReadHits<EDetectorID::Sts>(const PartitionedPODVector<HitTypes_t::at<EDetectorID::Sts>>&);
+// template void TrackingChain::ReadHits<EDetectorID::Sts>(const PartitionedPODVector<HitTypes_t::at<EDetectorID::Sts>>&);
diff --git a/algo/ca/TrackingChain.h b/algo/ca/TrackingChain.h
index c59f81f400304af2d52f2a896dc51c53ac2dbb29..e5dcaccec3154cbba13c89b2a8c760e5bb41e340 100644
--- a/algo/ca/TrackingChain.h
+++ b/algo/ca/TrackingChain.h
@@ -10,6 +10,7 @@
 #pragma once
 
 #include "TrackingDefs.h"
+#include "tof/Hit.h"
 
 #include <vector>
 
@@ -18,10 +19,10 @@
 #include "CaTrack.h"
 #include "CaTrackingMonitor.h"
 #include "CaVector.h"
-#include "PartitionedVector.h"
+#include "PartitionedSpan.h"
 #include "RecoResults.h"
 #include "SubChain.h"
-#include "data/sts/Hit.h"
+#include "sts/Hit.h"
 
 namespace cbm::algo
 {
@@ -31,6 +32,11 @@ namespace cbm::algo
   /// The class executes a tracking algorithm in the online data reconstruction chain.
   class TrackingChain : public SubChain {
   public:
+    struct Input_t {
+      PartitionedSpan<sts::Hit> stsHits;
+      PartitionedSpan<tof::Hit> tofHits;
+    };
+
     using Return_t = std::pair<ca::Vector<ca::Track>, ca::TrackingMonitorData>;
 
     /// \brief  Provides action in the initialization of the run
@@ -39,7 +45,7 @@ namespace cbm::algo
     /// \brief  Provides action for a given time-slice
     /// \param  recoResults  Structure of reconstruction results
     /// \return A pair (vector of tracks, tracking monitor)
-    Return_t Run(const RecoResults& recoResults);
+    Return_t Run(Input_t recoResults);
 
     /// \brief  Provides action in the end of the run
     void Finalize();
@@ -47,13 +53,13 @@ namespace cbm::algo
   private:
     /// \brief  Prepares input data
     /// \param  recoResults  Structure of reconstruction results
-    void PrepareInput(const RecoResults& recoResults);
+    void PrepareInput(Input_t recoResults);
 
     /// \brief  Reads from different detector subsystems
     /// \tparam DetID Detector ID
     /// \param  hits  Hits vector
     template<ca::EDetectorID DetID>
-    void ReadHits(const PartitionedPODVector<ca::HitTypes_t::at<DetID>>& hits);
+    void ReadHits(PartitionedSpan<const ca::HitTypes_t::at<DetID>> hits);
 
     ca::Framework fCaFramework {};      ///< CA framework instance
     ca::DataManager fCaDataManager {};  ///< CA data manager
diff --git a/algo/data/sts/Hit.h b/algo/data/sts/Hit.h
index 0a33355535b615cca69a1f6bee867468295b3bd5..091ba8db3c1688a28b6f76462a18c1a6cd329f4c 100644
--- a/algo/data/sts/Hit.h
+++ b/algo/data/sts/Hit.h
@@ -26,6 +26,16 @@ namespace cbm::algo::sts
     u32 fFrontClusterId;  ///< Index of front-side cluster, used by tracking to reduce combinatorics
     u32 fBackClusterId;   ///< Index of back-side cluster, used by tracking to reduce combinatorics
 
+    // Interface for tracking
+    real X() const { return fX; }
+    real Y() const { return fY; }
+    real Z() const { return fZ; }
+    u32 Time() const { return fTime; }
+
+    real Dx() const { return fDx; }
+    real Dy() const { return fDy; }
+    real TimeError() const { return fTimeError; }
+
   private:  // serialization
     friend class boost::serialization::access;
 
diff --git a/algo/detectors/tof/Clusterizer.cxx b/algo/detectors/tof/Clusterizer.cxx
index 14a9aa1e35e331aa5226352f5cb2cbaa395b94ef..ee370d980c6dc0f72d5bf1efe81ccf29209d84a3 100644
--- a/algo/detectors/tof/Clusterizer.cxx
+++ b/algo/detectors/tof/Clusterizer.cxx
@@ -120,8 +120,8 @@ namespace cbm::algo::tof
   Clusterizer::resultType Clusterizer::buildClusters(std::vector<inputType>& input)
   {
     // Hit variables
-    TofCluster cluster;
-    std::vector<TofCluster> clustersOut;
+    Hit cluster;
+    std::vector<Hit> clustersOut;
 
     // Reference cell of a cluster
     TofCell* cell = nullptr;
@@ -231,8 +231,8 @@ namespace cbm::algo::tof
     return clustersOut;
   }
 
-  bool Clusterizer::AddNextChan(std::vector<inputType>& input, int32_t lastChan, TofCluster& cluster,
-                                std::vector<TofCluster>& clustersOut, std::vector<inputType::iterator>* lastChanPos)
+  bool Clusterizer::AddNextChan(std::vector<inputType>& input, int32_t lastChan, Hit& cluster,
+                                std::vector<Hit>& clustersOut, std::vector<inputType::iterator>* lastChanPos)
   {
     //D.Smith 25.8.23: Why are "C" digis (position "2") not considered here?
 
diff --git a/algo/detectors/tof/Clusterizer.h b/algo/detectors/tof/Clusterizer.h
index 611a34c1d44ba53b40a558a4722d0a48cfc1cde3..6771ba8a410bdb0b829349a2387b8819af8f3100 100644
--- a/algo/detectors/tof/Clusterizer.h
+++ b/algo/detectors/tof/Clusterizer.h
@@ -12,9 +12,8 @@
 // TOF Classes and includes
 class CbmTofDigi;
 
-// ROOT Classes and includes
-#include "Math/Rotation3D.h"
-#include "Math/Vector3Dfwd.h"
+#include "ClusterizerPars.h"
+#include "Hit.h"
 
 // C++ Classes and includes
 #include <memory>
@@ -24,117 +23,9 @@ class CbmTofDigi;
 
 namespace cbm::algo::tof
 {
-  struct TofCell {
-    double sizeX, sizeY;
-    ROOT::Math::XYZVector pos;
-    ROOT::Math::Rotation3D rotation;
-  };
-
-  struct ClusterizerChanPar {
-    std::vector<double> fvCPTOff;               //[nbSide]
-    std::vector<double> fvCPTotGain;            //[nbSide]
-    std::vector<double> fvCPTotOff;             //[nbSide]
-    std::vector<std::vector<double>> fvCPWalk;  //[nbSide][nbWalkBins]
-    int32_t address;                            //unique address
-    TofCell cell;
-  };
-
-  struct ClusterizerRpcPar {
-    uint32_t fDeadStrips;
-    double fPosYMaxScal;
-    double fdMaxTimeDist;
-    double fdMaxSpaceDist;
-    double fSigVel;
-    double fTimeRes;
-    int32_t numClWalkBinX;
-    double TOTMax;
-    double TOTMin;
-    bool swapChannelSides;
-    double channelDeadtime;
-    double fCPTOffYBinWidth;
-    double fCPTOffYRange;
-    std::vector<double> fCPTOffY;  //[nBin]
-    std::vector<ClusterizerChanPar> fChanPar = {};
-  };
-
-  struct TofCluster {
-    ROOT::Math::XYZVector hitPos    = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
-    ROOT::Math::XYZVector hitPosErr = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
-    double hitTime                  = 0.0;
-    double hitTimeErr               = 0.0;
-    int32_t address                 = 0;
-
-    std::vector<int32_t> vDigiIndRef;
-    double weightsSum = 0.0;
-
-    int32_t numChan() { return vDigiIndRef.size() / 2; }
-
-    void reset()
-    {
-      vDigiIndRef.clear();
-      hitPos     = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
-      hitPosErr  = ROOT::Math::XYZVector(0.5, 0.5, 0.5);  //D.Smith 15.8.23: Set this back to zero eventually.
-      hitTime    = 0.0;
-      hitTimeErr = 0.0;
-      weightsSum = 0.0;
-      address    = 0;
-    }
-
-    void add(ROOT::Math::XYZVector pos, double time, double timeErr, double weight, int32_t digiIndA, int32_t digiIndB)
-    {
-      vDigiIndRef.push_back(digiIndA);
-      vDigiIndRef.push_back(digiIndB);
-      hitPos += pos * weight;
-      hitTime += time * weight;
-      hitTimeErr += timeErr * weight;
-      weightsSum += weight;
-    }
-
-    void normalize(double timeErr)
-    {
-      // a/=b is translated to a := a*(1/b) in the ROOT::Math::XYZVector class, which has a different
-      // rounding behavior than a := (a/b). In rare cases this leads to 1.000... becoming 0.999... inside
-      // the floor() operation in the finalize() function of this class, and results in a different
-      // channel being associated with the cluster. To reproduce the output of the old hit finder, we
-      // divide element-wise instead. Perhaps floor() should be replaced by round().
-      //////// hitPos /= weightsSum;
-
-      hitPos.SetXYZ(hitPos.X() / weightsSum, hitPos.Y() / weightsSum, hitPos.Z() / weightsSum);
-      hitTime /= weightsSum;
-      hitTimeErr = timeErr;
-    }
-
-    void finalize(const TofCell& trafoCell, const ClusterizerRpcPar& par)
-    {
-      //get hit channel
-      const int32_t channel = par.fChanPar.size() / 2 + floor(hitPos.X() / trafoCell.sizeX);
-
-      // D.Smith 15.8.23: This channel correction doesn't seem to be needed
-      //  if (channel < 0) channel = 0;
-      //  if (channel > par.fChanPar.size() - 1) channel = par.fChanPar.size() - 1;
-      address = par.fChanPar[channel].address;
-
-      //Calibrate hit time if applicable
-      if (par.fCPTOffYRange != 0) {
-        const double dBin = (hitPos.Y() + par.fCPTOffYRange) / par.fCPTOffYBinWidth;
-        const int i1      = floor(dBin);
-        const int i2      = ceil(dBin);
-        hitTime -= par.fCPTOffY[i1] + (dBin - i1) * (par.fCPTOffY[i2] - par.fCPTOffY[i1]);
-      }
-
-      //get offset by rotation to master frame. get hit position by adding offset to cell coordinates
-      hitPos = trafoCell.pos + trafoCell.rotation(hitPos);
-
-      // Simple errors, not properly done at all for now
-      // Right way of doing it should take into account the weight distribution
-      // and real system time resolution
-      hitPosErr = ROOT::Math::XYZVector(0.5, 0.5, 0.5);
-    }
-  };
-
   class Clusterizer {
   public:
-    typedef std::vector<TofCluster> resultType;
+    typedef std::vector<Hit> resultType;
     typedef std::vector<std::pair<CbmTofDigi*, int32_t>> inputType;
 
     /**
@@ -165,8 +56,8 @@ namespace cbm::algo::tof
 
     resultType buildClusters(std::vector<inputType>& input);
 
-    bool AddNextChan(std::vector<inputType>& input, int32_t iLastChan, TofCluster& cluster,
-                     std::vector<TofCluster>& clustersOut, std::vector<inputType::iterator>* lastChanPos = nullptr);
+    bool AddNextChan(std::vector<inputType>& input, int32_t iLastChan, Hit& cluster, std::vector<Hit>& clustersOut,
+                     std::vector<inputType::iterator>* lastChanPos = nullptr);
   };
 
 }  // namespace cbm::algo::tof
diff --git a/algo/detectors/tof/ClusterizerPars.h b/algo/detectors/tof/ClusterizerPars.h
new file mode 100644
index 0000000000000000000000000000000000000000..fea7d8036cd7e808bc55005de9cef1e696689455
--- /dev/null
+++ b/algo/detectors/tof/ClusterizerPars.h
@@ -0,0 +1,47 @@
+/* Copyright (C) 2022 Facility for Antiproton and Ion Research in Europe, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Dominik Smith [committer], Pierre-Alain Loizeau, Felix Weiglhofer */
+#pragma once
+
+#include <vector>
+
+#include "Math/Rotation3D.h"
+#include "Math/Vector3Dfwd.h"
+
+
+namespace cbm::algo::tof
+{
+  struct TofCell {
+    double sizeX, sizeY;
+    ROOT::Math::XYZVector pos;
+    ROOT::Math::Rotation3D rotation;
+  };
+
+  struct ClusterizerChanPar {
+    std::vector<double> fvCPTOff;               //[nbSide]
+    std::vector<double> fvCPTotGain;            //[nbSide]
+    std::vector<double> fvCPTotOff;             //[nbSide]
+    std::vector<std::vector<double>> fvCPWalk;  //[nbSide][nbWalkBins]
+    int32_t address;                            //unique address
+    TofCell cell;
+  };
+
+  struct ClusterizerRpcPar {
+    uint32_t fDeadStrips;
+    double fPosYMaxScal;
+    double fdMaxTimeDist;
+    double fdMaxSpaceDist;
+    double fSigVel;
+    double fTimeRes;
+    int32_t numClWalkBinX;
+    double TOTMax;
+    double TOTMin;
+    bool swapChannelSides;
+    double channelDeadtime;
+    double fCPTOffYBinWidth;
+    double fCPTOffYRange;
+    std::vector<double> fCPTOffY;  //[nBin]
+    std::vector<ClusterizerChanPar> fChanPar = {};
+  };
+
+}  // namespace cbm::algo::tof
diff --git a/algo/detectors/tof/Hit.h b/algo/detectors/tof/Hit.h
new file mode 100644
index 0000000000000000000000000000000000000000..23899e9a5d08fee05d13b35d0d949b2573a06914
--- /dev/null
+++ b/algo/detectors/tof/Hit.h
@@ -0,0 +1,105 @@
+/* Copyright (C) 2022 Facility for Antiproton and Ion Research in Europe, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Dominik Smith [committer], Pierre-Alain Loizeau, Felix Weiglhofer */
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+#include "ClusterizerPars.h"
+#include "Definitions.h"
+#include "Math/Rotation3D.h"
+#include "Math/Vector3Dfwd.h"
+
+namespace cbm::algo::tof
+{
+
+  struct Hit {
+    ROOT::Math::XYZVector hitPos    = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
+    ROOT::Math::XYZVector hitPosErr = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
+    double hitTime                  = 0.0;
+    double hitTimeErr               = 0.0;
+    int32_t address                 = 0;
+
+    std::vector<int32_t> vDigiIndRef;
+    double weightsSum = 0.0;
+
+    // Interface for tracker
+
+    real X() const { return hitPos.X(); }
+    real Y() const { return hitPos.Y(); }
+    real Z() const { return hitPos.Z(); }
+    u32 Time() const { return hitTime; }
+
+    real Dx() const { return hitPosErr.X(); }
+    real Dy() const { return hitPosErr.Y(); }
+    real TimeError() const { return hitTimeErr; }
+
+    // Interface end
+
+    int32_t numChan() { return vDigiIndRef.size() / 2; }
+
+    void reset()
+    {
+      vDigiIndRef.clear();
+      hitPos     = ROOT::Math::XYZVector(0.0, 0.0, 0.0);
+      hitPosErr  = ROOT::Math::XYZVector(0.5, 0.5, 0.5);  //D.Smith 15.8.23: Set this back to zero eventually.
+      hitTime    = 0.0;
+      hitTimeErr = 0.0;
+      weightsSum = 0.0;
+      address    = 0;
+    }
+
+    void add(ROOT::Math::XYZVector pos, double time, double timeErr, double weight, int32_t digiIndA, int32_t digiIndB)
+    {
+      vDigiIndRef.push_back(digiIndA);
+      vDigiIndRef.push_back(digiIndB);
+      hitPos += pos * weight;
+      hitTime += time * weight;
+      hitTimeErr += timeErr * weight;
+      weightsSum += weight;
+    }
+
+    void normalize(double timeErr)
+    {
+      // a/=b is translated to a := a*(1/b) in the ROOT::Math::XYZVector class, which has a different
+      // rounding behavior than a := (a/b). In rare cases this leads to 1.000... becoming 0.999... inside
+      // the floor() operation in the finalize() function of this class, and results in a different
+      // channel being associated with the cluster. To reproduce the output of the old hit finder, we
+      // divide element-wise instead. Perhaps floor() should be replaced by round().
+      //////// hitPos /= weightsSum;
+
+      hitPos.SetXYZ(hitPos.X() / weightsSum, hitPos.Y() / weightsSum, hitPos.Z() / weightsSum);
+      hitTime /= weightsSum;
+      hitTimeErr = timeErr;
+    }
+
+    void finalize(const TofCell& trafoCell, const ClusterizerRpcPar& par)
+    {
+      //get hit channel
+      const int32_t channel = par.fChanPar.size() / 2 + floor(hitPos.X() / trafoCell.sizeX);
+
+      // D.Smith 15.8.23: This channel correction doesn't seem to be needed
+      //  if (channel < 0) channel = 0;
+      //  if (channel > par.fChanPar.size() - 1) channel = par.fChanPar.size() - 1;
+      address = par.fChanPar[channel].address;
+
+      //Calibrate hit time if applicable
+      if (par.fCPTOffYRange != 0) {
+        const double dBin = (hitPos.Y() + par.fCPTOffYRange) / par.fCPTOffYBinWidth;
+        const int i1      = floor(dBin);
+        const int i2      = ceil(dBin);
+        hitTime -= par.fCPTOffY[i1] + (dBin - i1) * (par.fCPTOffY[i2] - par.fCPTOffY[i1]);
+      }
+
+      //get offset by rotation to master frame. get hit position by adding offset to cell coordinates
+      hitPos = trafoCell.pos + trafoCell.rotation(hitPos);
+
+      // Simple errors, not properly done at all for now
+      // Right way of doing it should take into account the weight distribution
+      // and real system time resolution
+      hitPosErr = ROOT::Math::XYZVector(0.5, 0.5, 0.5);
+    }
+  };
+
+}  // namespace cbm::algo::tof
diff --git a/algo/detectors/tof/Hitfind.cxx b/algo/detectors/tof/Hitfind.cxx
index ba1f4bdae6c39cce97f27654c0cd2710b8047a59..e1c1379171370714b3bf52ed95774df405683d0a 100644
--- a/algo/detectors/tof/Hitfind.cxx
+++ b/algo/detectors/tof/Hitfind.cxx
@@ -16,31 +16,39 @@ using fles::Subsystem;
 namespace cbm::algo::tof
 {
   // -----   Execution   -------------------------------------------------------
-  Hitfind::resultType Hitfind::operator()(std::vector<CbmTofDigi>* digiIn)
+  Hitfind::resultType Hitfind::operator()(gsl::span<CbmTofDigi> digiIn)
   {
     xpu::push_timer("TofHitfind");
 
     // --- Output data
     resultType result                  = {};
-    std::vector<TofCluster>& clusterTs = result.first;
-    HitfindMonitorData& monitor        = result.second;
+    auto& clusterTs                    = result.first;
+    auto& monitor                      = result.second;
 
     // Do calibration globally (optional, should not be used together with RPC-wise calibration)
-    *digiIn = CalibRawDigis(*digiIn);
+    CalibRawDigis(digiIn, monitor);
 
     // Loop over the digis array and store the Digis in separate vectors for
     // each RPC modules
-    for (int32_t idigi = 0; idigi < digiIn->size(); idigi++) {
-      CbmTofDigi* pDigi = &(digiIn->at(idigi));
+    for (int32_t idigi = 0; idigi < digiIn.size(); idigi++) {
+      CbmTofDigi* pDigi = &(digiIn[idigi]);
 
       // These are doubles in the digi class
       const double SmType = pDigi->GetType();
       const double Sm     = pDigi->GetSm();
       const double Rpc    = pDigi->GetRpc();
       const int NbRpc     = fNbRpc[SmType];
+      if (SmType >= fNbSm.size() || Sm * NbRpc + Rpc >= fNbSm[SmType] * NbRpc) {
+        // Error already counted for monitoring during Digis calibration, so just discard here
+        continue;
+      }
       fStorDigi[SmType][Sm * NbRpc + Rpc].push_back(std::make_pair(pDigi, idigi));
     }
 
+    std::vector<Hit> clustersFlat;
+    std::vector<size_t> rpcSizes;   // nClusters per RPC
+    std::vector<u32> rpcAddresses;  // RPC addresses
+
     // ---  RPC loop
     for (uint32_t SmType = 0; SmType < fNbSm.size(); SmType++) {
       const int NbRpc = fNbRpc[SmType];
@@ -52,10 +60,18 @@ namespace cbm::algo::tof
           std::vector<std::pair<CbmTofDigi*, int32_t>>& digiExp = fStorDigi[SmType][Sm * NbRpc + Rpc];
 
           // Build clusters
-          std::vector<cbm::algo::tof::TofCluster> clusters = fAlgo[SmType][Sm * NbRpc + Rpc](digiExp);
+          std::vector<cbm::algo::tof::Hit> clusters = fAlgo[SmType][Sm * NbRpc + Rpc](digiExp);
+
+          // Store hw address of partition
+          // TODO: is this correct? Does this address make sense?
+          u32 rpcAddress = CbmTofAddress::GetUniqueAddress(Sm, Rpc, 0, 0, SmType);
+          rpcAddresses.push_back(rpcAddress);
+
+          // store partition size
+          rpcSizes.push_back(clusters.size());
 
           // Append clusters to output
-          clusterTs.insert(clusterTs.end(), clusters.begin(), clusters.end());
+          clustersFlat.insert(clustersFlat.end(), clusters.begin(), clusters.end());
 
           // Clear digi storage
           digiExp.clear();
@@ -63,10 +79,15 @@ namespace cbm::algo::tof
       }
     }
 
+    // Monitoring
     xpu::timings timings = xpu::pop_timer();
     monitor.fTimeHitfind = timings.wall();
-    monitor.fNumDigis    = digiIn->size();
-    monitor.fNumHits     = clusterTs.size();
+    monitor.fNumDigis    = digiIn.size();
+    monitor.fNumHits     = clustersFlat.size();
+
+    // Create ouput vector
+    clusterTs = PartitionedVector(std::move(clustersFlat), rpcSizes, rpcAddresses);
+
     return result;
   }
   // ----------------------------------------------------------------------------
@@ -141,22 +162,28 @@ namespace cbm::algo::tof
 
 
   // -------------------------- Calibration -------------------------------------
-  std::vector<CbmTofDigi> Hitfind::CalibRawDigis(const std::vector<CbmTofDigi>& digiVec)
+  void Hitfind::CalibRawDigis(gsl::span<CbmTofDigi> digiVec, HitfindMonitorData& monitor)
   {
     // channel deadtime map
     std::map<int32_t, double> mChannelDeadTime;
-    std::vector<CbmTofDigi> calDigiVecOut;
 
-    for (int32_t iDigi = 0; iDigi < digiVec.size(); iDigi++) {
+    for (size_t iDigi = 0; iDigi < digiVec.size(); iDigi++) {
 
-      CbmTofDigi pDigi              = digiVec.at(iDigi);
+      CbmTofDigi pDigi              = digiVec[iDigi];
       const double SmType           = pDigi.GetType();
       const double Sm               = pDigi.GetSm();
       const double Rpc              = pDigi.GetRpc();
       const double Chan             = pDigi.GetChannel();
       const double Side             = pDigi.GetSide();
       const int NbRpc               = fNbRpc[SmType];
-      HitfindSetup::Rpc rpcPar      = fTofConfig.rpcs[SmType][Sm * NbRpc + Rpc];
+
+      auto& rpcs = fTofConfig.rpcs;
+      if (SmType >= rpcs.size() || Sm * NbRpc + Rpc >= rpcs.at(SmType).size()) {
+        monitor.fDigiCalibUnknownRPC++;
+        continue;
+      }
+
+      HitfindSetup::Rpc rpcPar      = fTofConfig.rpcs.at(SmType).at(Sm * NbRpc + Rpc);
       HitfindSetup::Channel chanPar = rpcPar.chanPar[Chan];
 
       if (rpcPar.swapChannelSides && 5 != SmType && 8 != SmType) {
@@ -203,14 +230,12 @@ namespace cbm::algo::tof
         if (0 < iWx) { dWT -= dDTot * (walk[iWx - 1] - walk[iWx]); }
       }
       pCalDigi.SetTime(pCalDigi.GetTime() - dWT);  // calibrate Digi Time
-      calDigiVecOut.push_back(pCalDigi);
+      digiVec[iDigi] = pCalDigi;
     }
 
     /// Sort the buffers of hits due to the time offsets applied
-    std::sort(calDigiVecOut.begin(), calDigiVecOut.end(),
+    std::sort(digiVec.begin(), digiVec.end(),
               [](const CbmTofDigi& a, const CbmTofDigi& b) -> bool { return a.GetTime() < b.GetTime(); });
-
-    return calDigiVecOut;
   }
 
 }  // namespace cbm::algo::tof
diff --git a/algo/detectors/tof/Hitfind.h b/algo/detectors/tof/Hitfind.h
index 09f761f4d524f7637124f71efba53504a0cfb35b..897ce36b8d5daefef64947ac24407219a6bcb37f 100644
--- a/algo/detectors/tof/Hitfind.h
+++ b/algo/detectors/tof/Hitfind.h
@@ -5,7 +5,7 @@
 #ifndef TOFHITFIND_H
 #define TOFHITFIND_H 1
 
-#include "Timeslice.hpp"
+#include "CbmTofDigi.h"
 
 #include "tof/Clusterizer.h"
 #include "tof/HitfindSetup.h"
@@ -15,8 +15,7 @@
 #include <sstream>
 #include <vector>
 
-#include "DigiData.h"
-#include "PODVector.h"
+#include "PartitionedVector.h"
 
 namespace cbm::algo::tof
 {
@@ -31,6 +30,7 @@ namespace cbm::algo::tof
     double fTimeHitfind = 0;
     size_t fNumDigis    = 0;
     size_t fNumHits     = 0;
+    size_t fDigiCalibUnknownRPC = 0;
 
     std::string print() const
     {
@@ -42,7 +42,7 @@ namespace cbm::algo::tof
   };
 
   /** @class Hitfind
- ** @brief Algo class for hitfinding 
+ ** @brief Algo class for hitfinding
  ** @author Dominik Smith <d.smith@gsi.de>
  ** @since 16.10.2023
  **
@@ -50,13 +50,15 @@ namespace cbm::algo::tof
   class Hitfind {
 
   public:
-    typedef std::pair<std::vector<TofCluster>, HitfindMonitorData> resultType;
+    typedef std::pair<PartitionedVector<Hit>, HitfindMonitorData> resultType;
 
     /** @brief Algorithm execution
      ** @param fles timeslice to hitfind
      ** @return pair: digi timeslice, monitoring data
+     **
+     ** @note Modifies input digis for time calibration
      **/
-    resultType operator()(std::vector<CbmTofDigi>* digiIn);
+    resultType operator()(gsl::span<CbmTofDigi> digiIn);
 
     /** @brief Default constructor **/
     Hitfind() {};
@@ -85,7 +87,7 @@ namespace cbm::algo::tof
     std::vector<int32_t> fNbRpc;
 
     /** @brief Applies calibration to input digis **/
-    std::vector<CbmTofDigi> CalibRawDigis(const std::vector<CbmTofDigi>& digiVec);
+    void CalibRawDigis(gsl::span<CbmTofDigi> digiVec, HitfindMonitorData& monitor);
   };
 }  // namespace cbm::algo::tof
 
diff --git a/algo/detectors/tof/HitfinderChain.cxx b/algo/detectors/tof/HitfinderChain.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..6d78c43b7cf88751373602ac87280a587edce73b
--- /dev/null
+++ b/algo/detectors/tof/HitfinderChain.cxx
@@ -0,0 +1,26 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#include "HitfinderChain.h"
+
+#include "config/Yaml.h"
+
+using namespace cbm::algo;
+using namespace cbm::algo::tof;
+
+void HitfinderChain::Init()
+{
+  auto setup           = config::ReadFromFile<HitfindSetup>(Opts().ParamsDir() / "TofHitfinderPar.yaml");
+  fHitfind             = std::make_unique<Hitfind>();
+  fHitfind->fTofConfig = setup;
+  fHitfind->Init();
+}
+
+HitfinderChain::ReturnType HitfinderChain::Run(gsl::span<CbmTofDigi> digis)
+{
+  auto ret = (*fHitfind)(digis);
+
+  L_(info) << "TS contains " << ret.first.NElements() << " TOF Hits";
+  L_(error) << "TOF Digis with unknown RPCs: " << ret.second.fDigiCalibUnknownRPC;
+  return ret;
+}
diff --git a/algo/detectors/tof/HitfinderChain.h b/algo/detectors/tof/HitfinderChain.h
new file mode 100644
index 0000000000000000000000000000000000000000..20d650aafcfdea44d587ed8d9652501a10e6a139
--- /dev/null
+++ b/algo/detectors/tof/HitfinderChain.h
@@ -0,0 +1,37 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#pragma once
+
+#include "CbmTofDigi.h"
+
+#include <gsl/span>
+#include <memory>
+
+#include "Hitfind.h"
+#include "HitfindSetup.h"
+#include "SubChain.h"
+
+namespace cbm::algo::tof
+{
+
+  class HitfinderChain : public SubChain {
+
+  public:
+    using ReturnType = Hitfind::resultType;
+
+    HitfinderChain()  = default;
+    ~HitfinderChain() = default;
+
+    void Init();
+
+    /**
+     * @brief Run the hitfinder and apply time calibration to digis
+     */
+    ReturnType Run(gsl::span<CbmTofDigi> digis);
+
+  private:
+    std::unique_ptr<Hitfind> fHitfind;
+  };
+
+}  // namespace cbm::algo::tof
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index c6ab97da629207f37c61e9369783b32c068de35c..54f169c521395cd154706668d8e5cdbc59551fa5 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -45,6 +45,7 @@ void Reco::Init(const Options& opts)
   SetContext(&fContext);
   fUnpack.SetContext(&fContext);
   fStsHitFinder.SetContext(&fContext);
+  fTofHitFinder.SetContext(&fContext);
   fTracking.SetContext(&fContext);
 
   if (Opts().HistogramUri() != "") {
@@ -84,6 +85,9 @@ void Reco::Init(const Options& opts)
   hitFinderPars.memory = Params().sts.memory;
   fStsHitFinder.SetParameters(hitFinderPars);
 
+  // TOF Hitfinder
+  fTofHitFinder.Init();
+
   // Tracking
   fTracking.Init();
 
@@ -124,6 +128,7 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
         case RecoParams::UnpackMode::CPU:
           unpackResult  = fUnpack.Run(ts);
           unpackMonitor = unpackResult.second;
+          QueueUnpackerMetrics(ts, unpackMonitor, unpackResult.first);
           break;
       }
     }
@@ -135,27 +140,36 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       auto [ev, mon] = fEventBuild->Run(unpackResult.first);
       events         = std::move(ev);
       evbuildMonitor = mon;
+      QueueEvbuildMetrics(evbuildMonitor);
     }
 
     sts::HitfinderMonitor stsHitfinderMonitor;
     PartitionedSpan<sts::Hit> stsHits;
     if (Opts().HasStep(Step::LocalReco) && Opts().HasDetector(fles::Subsystem::STS)) {
       auto stsResults     = fStsHitFinder(unpackResult.first.fSts);
-      stsHits             = std::move(stsResults.hits);
+      stsHits             = stsResults.hits;
       stsHitfinderMonitor = stsResults.monitor;
+      QueueStsRecoMetrics(stsHitfinderMonitor);
+    }
+
+    PartitionedVector<tof::Hit> tofHits;
+    if (Opts().HasStep(Step::LocalReco) && Opts().HasDetector(fles::Subsystem::TOF)) {
+      auto [hits, monitor] = fTofHitFinder.Run(unpackResult.first.fTof);
+      tofHits              = std::move(hits);
+      QueueTofRecoMetrics(monitor);
     }
 
     // --- Tracking
     if (Opts().HasStep(Step::Tracking)) {
-      TrackingChain::Return_t trackingOutput = fTracking.Run(results);
+      TrackingChain::Input_t input {
+        .stsHits = stsHits,
+        .tofHits = tofHits,
+      };
+      TrackingChain::Return_t trackingOutput = fTracking.Run(input);
       if (Opts().HasOutput(RecoData::Track)) { results.tracks = std::move(trackingOutput.first); }
       QueueTrackingMetrics(trackingOutput.second);
     }
 
-    QueueUnpackerMetrics(ts, unpackMonitor, unpackResult.first);
-    QueueStsRecoMetrics(stsHitfinderMonitor);
-    QueueEvbuildMetrics(evbuildMonitor);
-
     if (Opts().HasOutput(RecoData::DigiEvent)) results.events = std::move(events);
     if (Opts().HasOutput(RecoData::Hit)) results.stsHits = std::move(stsHits);
   }
@@ -261,6 +275,19 @@ void Reco::QueueStsRecoMetrics(const sts::HitfinderMonitor& monitor)
                            });
 }
 
+void Reco::QueueTofRecoMetrics(const tof::HitfindMonitorData& mon)
+{
+  if (!HasMonitor()) return;
+
+  GetMonitor().QueueMetric("cbmreco", {{"hostname", fles::system::current_hostname()}, {"child", Opts().ChildId()}},
+                           {
+                             {"tofRecoTimeTotal", mon.fTimeHitfind},
+                             {"tofRecoNumDigisIn", mon.fNumDigis},
+                             {"tofRecoNumHits", mon.fNumHits},
+                             {"tofRecoUnknownRPC", mon.fDigiCalibUnknownRPC},
+                           });
+}
+
 void Reco::QueueEvbuildMetrics(const evbuild::EventbuildChainMonitorData& mon)
 {
   if (!HasMonitor()) return;
diff --git a/algo/global/Reco.h b/algo/global/Reco.h
index 7932985b6bb93d755d58416755a708690fbe74ba..3fe138a22bb0e646b7d4254c2aa009fcc489dab0 100644
--- a/algo/global/Reco.h
+++ b/algo/global/Reco.h
@@ -4,6 +4,8 @@
 #ifndef CBM_ALGO_GLOBAL_RECO_H
 #define CBM_ALGO_GLOBAL_RECO_H
 
+#include "tof/HitfinderChain.h"
+
 #include <xpu/host.h>
 
 #include "EventbuildChain.h"
@@ -50,6 +52,10 @@ namespace cbm::algo
     UnpackChain fUnpack;
     sts::HitfinderChain fStsHitFinder;
 
+    // TOF
+    tof::HitfinderChain fTofHitFinder;
+
+    // Eventbuilding
     std::unique_ptr<evbuild::EventbuildChain> fEventBuild;
 
     // Tracking
@@ -59,6 +65,7 @@ namespace cbm::algo
 
     void QueueUnpackerMetrics(const fles::Timeslice&, const UnpackMonitorData&, const DigiData&);
     void QueueStsRecoMetrics(const sts::HitfinderMonitor&);
+    void QueueTofRecoMetrics(const tof::HitfindMonitorData&);
     void QueueEvbuildMetrics(const evbuild::EventbuildChainMonitorData&);
     void QueueTrackingMetrics(const ca::TrackingMonitorData&);
   };
diff --git a/algo/test/_GTestPartitionedSpan.cxx b/algo/test/_GTestPartitionedSpan.cxx
index 7afc70f50d6eb6a1728e606dab79701f62c0e032..4a730e27672644fb2b52a96e742713930325ef5f 100644
--- a/algo/test/_GTestPartitionedSpan.cxx
+++ b/algo/test/_GTestPartitionedSpan.cxx
@@ -9,7 +9,7 @@
 using namespace cbm::algo;
 
 template<typename T>
-void EXPECT_CONTAINER_EQ(gsl::span<T> a, std::vector<T> b)
+void EXPECT_CONTAINER_EQ(gsl::span<T> a, std::vector<i32> b)
 {
   EXPECT_EQ(a.size(), b.size());
   for (size_t i = 0; i < a.size(); ++i) {
@@ -36,7 +36,8 @@ protected:
     fAddressesInvalid = {0x0, 0x100};
   }
 
-  void Ensure(const PartitionedSpan<i32>& vec)
+  template<typename T>  // T is either i32 or 'const i32'
+  void Ensure(const PartitionedSpan<T>& vec)
   {
     EXPECT_EQ(vec.NElements(), 9);
     EXPECT_EQ(vec.NPartitions(), 3);
@@ -101,6 +102,14 @@ TEST_F(PartitionedSpanTest, IsCopyConstructable)
   Ensure(vecCopy);
 }
 
+TEST_F(PartitionedSpanTest, IsConstCopyConstructable)
+{
+  PartitionedSpan vec(fData, fOffsets, fAddresses);
+
+  PartitionedSpan<const i32> vecCopy(vec);
+  Ensure(vecCopy);
+}
+
 TEST_F(PartitionedSpanTest, ThrowsOnNumAddressesMismatchesNumPartions)
 {
   EXPECT_THROW(PartitionedSpan vec(fData, fOffsets, fAddressesInvalid), std::runtime_error);
diff --git a/reco/tasks/CbmTaskTofClusterizer.cxx b/reco/tasks/CbmTaskTofClusterizer.cxx
index 9868752d191c8fa3a9774d365f17de8323469413..a6eaf631d2644c8cfaaff78700f337973b8a9a66 100644
--- a/reco/tasks/CbmTaskTofClusterizer.cxx
+++ b/reco/tasks/CbmTaskTofClusterizer.cxx
@@ -352,10 +352,10 @@ bool CbmTaskTofClusterizer::BuildClusters()
   *fTofCalDigiVec = fTofDigiVec;
 
   //call cluster finder
-  std::vector<cbm::algo::tof::TofCluster> clusters = fAlgo(fTofCalDigiVec).first;
+  auto clusters = fAlgo(*fTofCalDigiVec).first;
 
   //Store hits and match
-  for (auto const& cluster : clusters) {
+  for (auto const& cluster : clusters.Data()) {
     const int32_t hitIndex = fTofHitsColl->GetEntriesFast();
     TVector3 hitpos        = TVector3(cluster.hitPos.X(), cluster.hitPos.Y(), cluster.hitPos.Z());
     TVector3 hiterr        = TVector3(cluster.hitPosErr.X(), cluster.hitPosErr.Y(), cluster.hitPosErr.Z());