diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs
index da6a1e25ba..d53e14e53b 100644
--- a/crates/chain/src/keychain/txout_index.rs
+++ b/crates/chain/src/keychain/txout_index.rs
@@ -5,12 +5,15 @@ use crate::{
     spk_iter::BIP32_MAX_INDEX,
     SpkIterator, SpkTxOutIndex,
 };
-use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
+use alloc::vec::Vec;
+use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
 use core::{
     fmt::Debug,
     ops::{Bound, RangeBounds},
 };
 
+use crate::local_chain::CheckPoint;
+use crate::spk_client::{FullScanRequest, SyncRequest};
 use crate::Append;
 
 const DEFAULT_LOOKAHEAD: u32 = 25;
@@ -100,7 +103,7 @@ const DEFAULT_LOOKAHEAD: u32 = 25;
 /// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter
 /// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters
 #[derive(Clone, Debug)]
-pub struct KeychainTxOutIndex<K> {
+pub struct KeychainTxOutIndex<K: Clone + Ord + Send> {
     inner: SpkTxOutIndex<(K, u32)>,
     // descriptors of each keychain
     keychains: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
@@ -110,13 +113,13 @@ pub struct KeychainTxOutIndex<K> {
     lookahead: u32,
 }
 
-impl<K> Default for KeychainTxOutIndex<K> {
+impl<K: Clone + Ord + Debug + Send> Default for KeychainTxOutIndex<K> {
     fn default() -> Self {
         Self::new(DEFAULT_LOOKAHEAD)
     }
 }
 
-impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
+impl<K: Clone + Ord + Debug + Send> Indexer for KeychainTxOutIndex<K> {
     type ChangeSet = super::ChangeSet<K>;
 
     fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
@@ -134,20 +137,20 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
         changeset
     }
 
-    fn initial_changeset(&self) -> Self::ChangeSet {
-        super::ChangeSet(self.last_revealed.clone())
-    }
-
     fn apply_changeset(&mut self, changeset: Self::ChangeSet) {
         self.apply_changeset(changeset)
     }
 
+    fn initial_changeset(&self) -> Self::ChangeSet {
+        super::ChangeSet(self.last_revealed.clone())
+    }
+
     fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
         self.inner.is_relevant(tx)
     }
 }
 
-impl<K> KeychainTxOutIndex<K> {
+impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> {
     /// Construct a [`KeychainTxOutIndex`] with the given `lookahead`.
     ///
     /// The `lookahead` is the number of script pubkeys to derive and cache from the internal
@@ -169,7 +172,7 @@ impl<K> KeychainTxOutIndex<K> {
 }
 
 /// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].
-impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
+impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> {
     /// Return a reference to the internal [`SpkTxOutIndex`].
     ///
     /// **WARNING:** The internal index will contain lookahead spks. Refer to
@@ -291,7 +294,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     }
 }
 
-impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
+impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> {
     /// Return a reference to the internal map of keychain to descriptors.
     pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
         &self.keychains
@@ -664,6 +667,42 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
             .collect()
     }
 
+    /// Create a [`SyncRequest`] for this [`KeychainTxOutIndex`] for all revealed spks.
+    ///
+    /// This is the first step when performing a spk-based wallet sync, the returned [`SyncRequest`] collects
+    /// all revealed script pub keys needed to start a blockchain sync with a spk based blockchain client. A
+    /// [`CheckPoint`] representing the current chain tip must be provided.
+    pub fn sync_revealed_spks_request(&self, chain_tip: CheckPoint) -> SyncRequest {
+        // Sync all revealed SPKs
+        let spks = self
+            .revealed_spks()
+            .map(|(_keychain, _index, script)| ScriptBuf::from(script))
+            .collect::<Vec<ScriptBuf>>();
+
+        let mut req = SyncRequest::new(chain_tip);
+        req.add_spks(spks);
+        req
+    }
+
+    /// Create a [`FullScanRequest`] for this [`KeychainTxOutIndex`].
+    ///
+    /// This is the first step when performing a spk-based full scan, the returned [`FullScanRequest`]
+    /// collects iterators for the index's keychain script pub keys to start a blockchain full scan with a
+    /// spk based blockchain client. A [`CheckPoint`] representing the current chain tip must be provided.
+    ///
+    /// This operation is generally only used when importing or restoring previously used keychains
+    /// in which the list of used scripts is not known.
+    pub fn full_scan_request(
+        &self,
+        chain_tip: CheckPoint,
+    ) -> FullScanRequest<K, impl Iterator<Item = (u32, ScriptBuf)> + Clone> {
+        let spks_by_keychain = self.all_unbounded_spk_iters();
+
+        let mut req = FullScanRequest::new(chain_tip);
+        req.add_spks_by_keychain(spks_by_keychain);
+        req
+    }
+
     /// Applies the derivation changeset to the [`KeychainTxOutIndex`], extending the number of
     /// derived scripts per keychain, as specified in the `changeset`.
     pub fn apply_changeset(&mut self, changeset: super::ChangeSet<K>) {
diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs
index 2065669714..9d2cb06eba 100644
--- a/crates/chain/src/lib.rs
+++ b/crates/chain/src/lib.rs
@@ -52,6 +52,9 @@ mod spk_iter;
 #[cfg(feature = "miniscript")]
 pub use spk_iter::*;
 
+/// Helper types for use with spk-based blockchain clients.
+pub mod spk_client;
+
 #[allow(unused_imports)]
 #[macro_use]
 extern crate alloc;
diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs
new file mode 100644
index 0000000000..03331a1d37
--- /dev/null
+++ b/crates/chain/src/spk_client.rs
@@ -0,0 +1,182 @@
+use crate::collections::BTreeMap;
+use crate::local_chain::CheckPoint;
+use crate::{local_chain, ConfirmationTimeHeightAnchor, TxGraph};
+use alloc::{boxed::Box, vec::Vec};
+use bitcoin::{OutPoint, ScriptBuf, Txid};
+use core::default::Default;
+
+type InspectSpkFn = Box<dyn FnMut(&ScriptBuf) + Send>;
+type InspectTxidFn = Box<dyn FnMut(&Txid) + Send>;
+type InspectOutPointFn = Box<dyn FnMut(&OutPoint) + Send>;
+
+/// Helper types for use with spk-based blockchain clients.
+
+/// Data required to perform a spk-based blockchain client sync.
+///
+/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and
+/// outpoints. The sync process also updates the chain from the given [`CheckPoint`].
+pub struct SyncRequest {
+    /// A checkpoint for the current chain tip.
+    /// The full scan process will return a new chain update that extends this tip.
+    pub chain_tip: CheckPoint,
+    /// Transactions that spend from or to these script pubkeys.
+    spks: Vec<ScriptBuf>,
+    /// Transactions with these txids.
+    txids: Vec<Txid>,
+    /// Transactions with these outpoints or spend from these outpoints.
+    outpoints: Vec<OutPoint>,
+    /// An optional call-back function to inspect sync'd spks
+    inspect_spks: Option<InspectSpkFn>,
+    /// An optional call-back function to inspect sync'd txids
+    inspect_txids: Option<InspectTxidFn>,
+    /// An optional call-back function to inspect sync'd outpoints
+    inspect_outpoints: Option<InspectOutPointFn>,
+}
+
+fn null_inspect_spks(_spk: &ScriptBuf) {}
+fn null_inspect_txids(_txid: &Txid) {}
+fn null_inspect_outpoints(_outpoint: &OutPoint) {}
+
+impl SyncRequest {
+    /// Create a new [`SyncRequest`] from the current chain tip [`CheckPoint`].
+    pub fn new(chain_tip: CheckPoint) -> Self {
+        Self {
+            chain_tip,
+            spks: Default::default(),
+            txids: Default::default(),
+            outpoints: Default::default(),
+            inspect_spks: Default::default(),
+            inspect_txids: Default::default(),
+            inspect_outpoints: Default::default(),
+        }
+    }
+
+    /// Add [`ScriptBuf`]s to be sync'd with this request.
+    pub fn add_spks(&mut self, spks: impl IntoIterator<Item = ScriptBuf>) {
+        self.spks.extend(spks.into_iter())
+    }
+
+    /// Take the [`ScriptBuf`]s to be sync'd with this request.
+    pub fn take_spks(&mut self) -> impl Iterator<Item = ScriptBuf> {
+        let spks = core::mem::take(&mut self.spks);
+        let mut inspect = self
+            .inspect_spks
+            .take()
+            .unwrap_or(Box::new(null_inspect_spks));
+        spks.into_iter().inspect(move |s| inspect(s))
+    }
+
+    /// Add a function that will be called for each [`ScriptBuf`] sync'd in this request.
+    pub fn inspect_spks(&mut self, inspect: impl FnMut(&ScriptBuf) + Send + 'static) {
+        self.inspect_spks = Some(Box::new(inspect))
+    }
+
+    /// Add [`Txid`]s to be sync'd with this request.
+    pub fn add_txids(&mut self, txids: impl IntoIterator<Item = Txid>) {
+        self.txids.extend(txids.into_iter())
+    }
+
+    /// Take the [`Txid`]s to be sync'd with this request.
+    pub fn take_txids(&mut self) -> impl Iterator<Item = Txid> {
+        let txids = core::mem::take(&mut self.txids);
+        let mut inspect = self
+            .inspect_txids
+            .take()
+            .unwrap_or(Box::new(null_inspect_txids));
+        txids.into_iter().inspect(move |t| inspect(t))
+    }
+
+    /// Add a function that will be called for each [`Txid`] sync'd in this request.
+    pub fn inspect_txids(&mut self, inspect: impl FnMut(&Txid) + Send + 'static) {
+        self.inspect_txids = Some(Box::new(inspect))
+    }
+
+    /// Add [`OutPoint`]s to be sync'd with this request.
+    pub fn add_outpoints(&mut self, outpoints: impl IntoIterator<Item = OutPoint>) {
+        self.outpoints.extend(outpoints.into_iter())
+    }
+
+    /// Take the [`OutPoint`]s to be sync'd with this request.
+    pub fn take_outpoints(&mut self) -> impl Iterator<Item = OutPoint> {
+        let outpoints = core::mem::take(&mut self.outpoints);
+        let mut inspect = self
+            .inspect_outpoints
+            .take()
+            .unwrap_or(Box::new(null_inspect_outpoints));
+        outpoints.into_iter().inspect(move |o| inspect(o))
+    }
+
+    /// Add a function that will be called for each [`OutPoint`] sync'd in this request.
+    pub fn inspect_outpoints(&mut self, inspect: impl FnMut(&OutPoint) + Send + 'static) {
+        self.inspect_outpoints = Some(Box::new(inspect))
+    }
+}
+
+/// Data returned from a spk-based blockchain client sync.
+///
+/// See also [`SyncRequest`].
+pub struct SyncResult {
+    /// [`TxGraph`] update.
+    pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
+    /// [`LocalChain`] update.
+    ///
+    /// [`LocalChain`]: local_chain::LocalChain
+    pub chain_update: local_chain::Update,
+}
+
+/// Data required to perform a spk-based blockchain client full scan.
+///
+/// A client full scan iterates through all the scripts for the given keychains, fetching relevant
+/// data until some stop gap number of scripts is found that have no data. This operation is
+/// generally only used when importing or restoring previously used keychains in which the list of
+/// used scripts is not known. The full scan process also updates the chain from the given [`CheckPoint`].
+#[derive(Debug, Clone)]
+pub struct FullScanRequest<K, I> {
+    /// A checkpoint for the current chain tip. The full scan process will return a new chain update that extends this tip.
+    pub chain_tip: CheckPoint,
+    /// Iterators of script pubkeys indexed by the keychain index.
+    spks_by_keychain: BTreeMap<K, I>,
+}
+
+/// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`].
+impl<
+        K: Ord + Clone + Send,
+        I: IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
+    > FullScanRequest<K, I>
+{
+    /// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`].
+    pub fn new(chain_tip: CheckPoint) -> Self {
+        Self {
+            chain_tip,
+            spks_by_keychain: Default::default(),
+        }
+    }
+
+    /// Add map of keychain's to tuple of index, [`ScriptBuf`] iterators to be scanned with this
+    /// request.
+    ///
+    /// Adding a map with a keychain that has already been added will overwrite the previously added
+    /// keychain [`ScriptBuf`] iterator.
+    pub fn add_spks_by_keychain(&mut self, spks_by_keychain: BTreeMap<K, I>) {
+        self.spks_by_keychain.extend(spks_by_keychain)
+    }
+
+    /// Take the map of keychain, [`ScriptBuf`]s to be full scanned with this request.
+    pub fn take_spks_by_keychain(&mut self) -> BTreeMap<K, I> {
+        core::mem::take(&mut self.spks_by_keychain)
+    }
+}
+
+/// Data returned from a spk-based blockchain client full scan.
+///
+/// See also [`FullScanRequest`].
+pub struct FullScanResult<K: Ord + Clone + Send> {
+    /// [`TxGraph`] update.
+    pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
+    /// [`LocalChain`] update.
+    ///
+    /// [`LocalChain`]: local_chain::LocalChain
+    pub chain_update: local_chain::Update,
+    /// Map of keychain last active indices.
+    pub last_active_indices: BTreeMap<K, u32>,
+}
diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs
index 34cbccf5ce..16e158c41c 100644
--- a/crates/chain/src/tx_graph.rs
+++ b/crates/chain/src/tx_graph.rs
@@ -723,11 +723,6 @@ impl<A: Anchor> TxGraph<A> {
                     }
                     !has_missing_height
                 });
-                #[cfg(feature = "std")]
-                debug_assert!({
-                    println!("txid={} skip={}", txid, skip);
-                    true
-                });
                 !skip
             })
             .filter_map(move |(a, _)| {
@@ -743,6 +738,24 @@ impl<A: Anchor> TxGraph<A> {
             })
     }
 
+    /// Iterates over the heights of that the transaction anchors in this tx graph greater than a minimum height.
+    ///
+    /// This is useful if you want to find which heights you need to fetch data about in order to
+    /// confirm or exclude these anchors.
+    ///
+    /// See also: [`Self::missing_heights`]
+    pub fn anchor_heights(&self, min_height: u32) -> impl Iterator<Item = u32> + '_ {
+        let mut dedup = None;
+        self.anchors
+            .iter()
+            .map(|(a, _)| a.anchor_block().height)
+            .filter(move |height| {
+                let duplicate = dedup == Some(*height);
+                dedup = Some(*height);
+                !duplicate && *height > min_height
+            })
+    }
+
     /// Get the position of the transaction in `chain` with tip `chain_tip`.
     ///
     /// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.