diff --git a/.github/workflows/autorelease.yml b/.github/workflows/autorelease.yml
index 5a9d39e..35271a3 100644
--- a/.github/workflows/autorelease.yml
+++ b/.github/workflows/autorelease.yml
@@ -1,44 +1,17 @@
-name: Tag New Release
+name: Create Rust Release
+
 on:
-    push:
-      branches:
-        - main
-      paths:
-        - 'Cargo.toml'
-  
-permissions:
-    contents: write
-    id-token: write
+  push:
+    branches:
+    - main
+    paths:
+    - 'Cargo.toml'
+
+permissions: # these permissions are required for the actions to run
+  contents: write
 
 jobs:
-    verify-new-release-needed:
-        name: Verify new release needed
-        runs-on: ubuntu-latest
-        environment: dev
-        steps:
-            - name: Checkout code
-              uses: actions/checkout@v4
-              with: 
-                fetch-tags: "true"
-                fetch-depth: "0"
-            - name: Verify new release needed
-              env:
-                GH_TOKEN: ${{ github.token }}
-              run: |
-                echo "Checking if a new release is needed"
-                # get current version from cargo
-                CARGO_VER="v$(cargo metadata --format-version=1 --no-deps  | jq '.packages[] | select(.name == "zenith-builder-example") | .version' -r)"
-                echo "$CARGO_VER"
-                # get latest version from git tags
-                GIT_VER=$(git describe --tags $(git rev-list --tags --max-count=1))
-                echo "$GIT_VER"
-                if [ "$CARGO_VER" == "$GIT_VER" ]; then
-                echo "# No new release needed" >> $GITHUB_STEP_SUMMARY
-                exit 0
-                else
-                    echo "New release needed"
-                    gh release create "$CARGO_VER" -t "$CARGO_VER" --generate-notes
-                    ## Add the release information to the github actions summary
-                    echo "# New Release Created" >> $GITHUB_STEP_SUMMARY
-                    echo "Tag: $CARGO_VER" >> $GITHUB_STEP_SUMMARY
-                fi
+  auto-release: 
+    uses: init4tech/actions/.github/workflows/auto-release-rust.yml@main
+    with:
+      binary-name: 'zenith-builder-example'
diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index 66a8553..1fa97d5 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -9,3 +9,8 @@ on:
 jobs:
   rust-base:
     uses: init4tech/actions/.github/workflows/rust-base.yml@main
+    with: 
+      requires-private-deps: true
+    secrets: 
+      SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
+      
diff --git a/Cargo.toml b/Cargo.toml
index 913b3f1..c3782eb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,6 +32,9 @@ aws-sdk-kms = "1.15.0"
 hex = { package = "const-hex", version = "1", default-features = false, features = [
     "alloc",
 ] }
+
+signet-types = { git = "ssh://git@github.com/init4tech/signet-node.git" }
+
 serde = { version = "1.0.197", features = ["derive"] }
 tracing = "0.1.40"
 
@@ -41,7 +44,7 @@ openssl = { version = "0.10", features = ["vendored"] }
 reqwest = { version = "0.11.24", features = ["blocking", "json"] }
 ruint = "1.12.1"
 serde_json = "1.0"
-thiserror = "1.0.58"
+thiserror = "1.0.68"
 tokio = { version = "1.36.0", features = ["full", "macros", "rt-multi-thread"] }
 tracing-subscriber = "0.3.18"
 
diff --git a/bin/builder.rs b/bin/builder.rs
index d805b53..e18b333 100644
--- a/bin/builder.rs
+++ b/bin/builder.rs
@@ -2,6 +2,8 @@
 
 use builder::config::BuilderConfig;
 use builder::service::serve_builder_with_span;
+use builder::tasks::bundler::BundlePoller;
+use builder::tasks::oauth::Authenticator;
 use builder::tasks::tx_poller::TxPoller;
 
 use tokio::select;
@@ -11,8 +13,9 @@ async fn main() -> eyre::Result<()> {
     tracing_subscriber::fmt::try_init().unwrap();
     let span = tracing::info_span!("zenith-builder");
 
-    let config = BuilderConfig::load_from_env()?;
+    let config = BuilderConfig::load_from_env()?.clone();
     let provider = config.connect_provider().await?;
+    let authenticator = Authenticator::new(&config);
 
     tracing::debug!(
         rpc_url = config.host_rpc_url.as_ref(),
@@ -23,23 +26,26 @@ async fn main() -> eyre::Result<()> {
     let zenith = config.connect_zenith(provider.clone());
 
     let port = config.builder_port;
-
     let tx_poller = TxPoller::new(&config);
+    let bundle_poller = BundlePoller::new(&config, authenticator.clone()).await;
     let builder = builder::tasks::block::BlockBuilder::new(&config);
 
     let submit = builder::tasks::submit::SubmitTask {
+        authenticator: authenticator.clone(),
         provider,
         zenith,
         client: reqwest::Client::new(),
         sequencer_signer,
-        config,
+        config: config.clone(),
     };
 
+    let authenticator_jh = authenticator.spawn();
     let (submit_channel, submit_jh) = submit.spawn();
-    let (build_channel, build_jh) = builder.spawn(submit_channel);
-    let tx_poller_jh = tx_poller.spawn(build_channel.clone());
+    let (tx_channel, bundle_channel, build_jh) = builder.spawn(submit_channel);
+    let tx_poller_jh = tx_poller.spawn(tx_channel.clone());
+    let bundle_poller_jh = bundle_poller.spawn(bundle_channel);
 
-    let server = serve_builder_with_span(build_channel, ([0, 0, 0, 0], port), span);
+    let server = serve_builder_with_span(tx_channel, ([0, 0, 0, 0], port), span);
 
     select! {
         _ = submit_jh => {
@@ -54,6 +60,12 @@ async fn main() -> eyre::Result<()> {
         _ = tx_poller_jh => {
             tracing::info!("tx_poller finished");
         }
+        _ = bundle_poller_jh => {
+            tracing::info!("bundle_poller finished");
+        }
+        _ = authenticator_jh => {
+            tracing::info!("authenticator finished");
+        }
     }
 
     tracing::info!("shutting down");
diff --git a/src/config.rs b/src/config.rs
index 73acc32..862c615 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -26,6 +26,7 @@ const BUILDER_REWARDS_ADDRESS: &str = "BUILDER_REWARDS_ADDRESS";
 const ROLLUP_BLOCK_GAS_LIMIT: &str = "ROLLUP_BLOCK_GAS_LIMIT";
 const TX_POOL_URL: &str = "TX_POOL_URL";
 const TX_POOL_POLL_INTERVAL: &str = "TX_POOL_POLL_INTERVAL";
+const AUTH_TOKEN_REFRESH_INTERVAL: &str = "AUTH_TOKEN_REFRESH_INTERVAL";
 const TX_POOL_CACHE_DURATION: &str = "TX_POOL_CACHE_DURATION";
 const OAUTH_CLIENT_ID: &str = "OAUTH_CLIENT_ID";
 const OAUTH_CLIENT_SECRET: &str = "OAUTH_CLIENT_SECRET";
@@ -82,6 +83,8 @@ pub struct BuilderConfig {
     pub oauth_token_url: String,
     /// OAuth audience for the builder.
     pub oauth_audience: String,
+    /// The oauth token refresh interval in seconds.
+    pub oauth_token_refresh_interval: u64,
 }
 
 #[derive(Debug, thiserror::Error)]
@@ -159,6 +162,7 @@ impl BuilderConfig {
             oauth_authenticate_url: load_string(OAUTH_AUTHENTICATE_URL)?,
             oauth_token_url: load_string(OAUTH_TOKEN_URL)?,
             oauth_audience: load_string(OAUTH_AUDIENCE)?,
+            oauth_token_refresh_interval: load_u64(AUTH_TOKEN_REFRESH_INTERVAL)?,
         })
     }
 
diff --git a/src/service.rs b/src/service.rs
index 8182151..68fdb5c 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -105,7 +105,6 @@ pub async fn ingest_raw_handler(
 ) -> Result<Response, AppError> {
     let body = body.strip_prefix("0x").unwrap_or(&body);
     let buf = hex::decode(body).map_err(AppError::bad_req)?;
-
     let envelope = TxEnvelope::decode_2718(&mut buf.as_slice()).map_err(AppError::bad_req)?;
 
     ingest_handler(State(state), Json(envelope)).await
diff --git a/src/tasks/block.rs b/src/tasks/block.rs
index 07e0542..1836f0d 100644
--- a/src/tasks/block.rs
+++ b/src/tasks/block.rs
@@ -1,5 +1,9 @@
-use alloy::consensus::{SidecarBuilder, SidecarCoder, TxEnvelope};
+use alloy::{
+    consensus::{SidecarBuilder, SidecarCoder, TxEnvelope},
+    eips::eip2718::Decodable2718,
+};
 use alloy_primitives::{keccak256, Bytes, B256};
+use alloy_rlp::Buf;
 use std::{sync::OnceLock, time::Duration};
 use tokio::{select, sync::mpsc, task::JoinHandle};
 use tracing::Instrument;
@@ -7,6 +11,8 @@ use zenith_types::{encode_txns, Alloy2718Coder};
 
 use crate::config::BuilderConfig;
 
+use super::bundler::Bundle;
+
 #[derive(Debug, Default, Clone)]
 /// A block in progress.
 pub struct InProgressBlock {
@@ -57,6 +63,29 @@ impl InProgressBlock {
         self.transactions.push(tx.clone());
     }
 
+    /// Ingest a bundle into the in-progress block.
+    /// Ignores Signed Orders for now.
+    pub fn ingest_bundle(&mut self, bundle: Bundle) {
+        tracing::info!(bundle = %bundle.id, "ingesting bundle");
+
+        let txs = bundle
+            .bundle
+            .bundle
+            .txs
+            .into_iter()
+            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
+            .collect::<Result<Vec<_>, _>>();
+
+        if let Ok(txs) = txs {
+            self.unseal();
+            // extend the transactions with the decoded transactions.
+            // As this builder does not provide bundles landing "top of block", its fine to just extend.
+            self.transactions.extend(txs);
+        } else {
+            tracing::error!("failed to decode bundle. dropping");
+        }
+    }
+
     /// Encode the in-progress block
     fn encode_raw(&self) -> &Bytes {
         self.seal();
@@ -102,10 +131,15 @@ impl BlockBuilder {
     pub fn spawn(
         self,
         outbound: mpsc::UnboundedSender<InProgressBlock>,
-    ) -> (mpsc::UnboundedSender<TxEnvelope>, JoinHandle<()>) {
+    ) -> (
+        mpsc::UnboundedSender<TxEnvelope>,
+        mpsc::UnboundedSender<Bundle>,
+        JoinHandle<()>,
+    ) {
         let mut in_progress = InProgressBlock::default();
 
-        let (sender, mut inbound) = mpsc::unbounded_channel();
+        let (tx_sender, mut tx_inbound) = mpsc::unbounded_channel();
+        let (bundle_sender, mut bundle_inbound) = mpsc::unbounded_channel();
 
         let mut sleep = Box::pin(tokio::time::sleep(Duration::from_secs(
             self.incoming_transactions_buffer,
@@ -131,9 +165,18 @@ impl BlockBuilder {
                             // irrespective of whether we have any blocks to build.
                             sleep.as_mut().reset(tokio::time::Instant::now() + Duration::from_secs(self.incoming_transactions_buffer));
                         }
-                        item_res = inbound.recv() => {
-                            match item_res {
-                                Some(item) => in_progress.ingest_tx(&item),
+                        tx_resp = tx_inbound.recv() => {
+                            match tx_resp {
+                                Some(tx) => in_progress.ingest_tx(&tx),
+                                None => {
+                                    tracing::debug!("upstream task gone");
+                                    break
+                                }
+                            }
+                        }
+                        bundle_resp = bundle_inbound.recv() => {
+                            match bundle_resp {
+                                Some(bundle) => in_progress.ingest_bundle(bundle),
                                 None => {
                                     tracing::debug!("upstream task gone");
                                     break
@@ -146,6 +189,6 @@ impl BlockBuilder {
             .in_current_span(),
         );
 
-        (sender, handle)
+        (tx_sender, bundle_sender, handle)
     }
 }
diff --git a/src/tasks/bundler.rs b/src/tasks/bundler.rs
new file mode 100644
index 0000000..dcfea56
--- /dev/null
+++ b/src/tasks/bundler.rs
@@ -0,0 +1,127 @@
+//! Bundler service responsible for polling and submitting bundles to the in-progress block.
+use std::time::{Duration, Instant};
+
+pub use crate::config::BuilderConfig;
+use alloy_primitives::map::HashMap;
+use reqwest::Url;
+use serde::{Deserialize, Serialize};
+use signet_types::SignetEthBundle;
+use tokio::{sync::mpsc, task::JoinHandle};
+use tracing::debug;
+
+use oauth2::TokenResponse;
+
+use super::oauth::Authenticator;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Bundle {
+    pub id: String,
+    pub bundle: SignetEthBundle,
+}
+
+/// Response from the tx-pool containing a list of bundles.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TxPoolBundleResponse {
+    pub bundles: Vec<Bundle>,
+}
+
+pub struct BundlePoller {
+    pub config: BuilderConfig,
+    pub authenticator: Authenticator,
+    pub seen_uuids: HashMap<String, Instant>,
+}
+
+/// Implements a poller for the block builder to pull bundles from the tx cache.
+impl BundlePoller {
+    /// Creates a new BundlePoller from the provided builder config.
+    pub async fn new(config: &BuilderConfig, authenticator: Authenticator) -> Self {
+        Self {
+            config: config.clone(),
+            authenticator,
+            seen_uuids: HashMap::new(),
+        }
+    }
+
+    /// Fetches bundles from the transaction cache and returns the (oldest? random?) bundle in the cache.
+    pub async fn check_bundle_cache(&mut self) -> eyre::Result<Vec<Bundle>> {
+        let mut unique: Vec<Bundle> = Vec::new();
+
+        let bundle_url: Url = Url::parse(&self.config.tx_pool_url)?.join("bundles")?;
+        let token = self.authenticator.fetch_oauth_token().await?;
+
+        // Add the token to the request headers
+        let result = reqwest::Client::new()
+            .get(bundle_url)
+            .bearer_auth(token.access_token().secret())
+            .send()
+            .await?
+            .error_for_status()?;
+
+        let body = result.bytes().await?;
+        let bundles: TxPoolBundleResponse = serde_json::from_slice(&body)?;
+
+        bundles.bundles.iter().for_each(|bundle| {
+            self.check_seen_bundles(bundle.clone(), &mut unique);
+        });
+
+        Ok(unique)
+    }
+
+    /// Checks if the bundle has been seen before and if not, adds it to the unique bundles list.
+    fn check_seen_bundles(&mut self, bundle: Bundle, unique: &mut Vec<Bundle>) {
+        self.seen_uuids.entry(bundle.id.clone()).or_insert_with(|| {
+            // add to the set of unique bundles
+            unique.push(bundle.clone());
+            Instant::now() + Duration::from_secs(self.config.tx_pool_cache_duration)
+        });
+    }
+
+    /// Evicts expired bundles from the cache.
+    fn evict(&mut self) {
+        let expired_keys: Vec<String> = self
+            .seen_uuids
+            .iter()
+            .filter_map(|(key, expiry)| {
+                if expiry.elapsed().is_zero() {
+                    Some(key.clone())
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        for key in expired_keys {
+            self.seen_uuids.remove(&key);
+        }
+    }
+
+    pub fn spawn(mut self, bundle_channel: mpsc::UnboundedSender<Bundle>) -> JoinHandle<()> {
+        let handle: JoinHandle<()> = tokio::spawn(async move {
+            loop {
+                let bundle_channel = bundle_channel.clone();
+                let bundles = self.check_bundle_cache().await;
+
+                match bundles {
+                    Ok(bundles) => {
+                        for bundle in bundles {
+                            let result = bundle_channel.send(bundle);
+                            if result.is_err() {
+                                tracing::debug!("bundle_channel failed to send bundle");
+                            }
+                        }
+                    }
+                    Err(err) => {
+                        debug!(?err, "error fetching bundles from tx-pool");
+                    }
+                }
+
+                // evict expired bundles once every loop
+                self.evict();
+
+                tokio::time::sleep(Duration::from_secs(self.config.tx_pool_poll_interval)).await;
+            }
+        });
+
+        handle
+    }
+}
diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs
index 1df8ef3..3692d13 100644
--- a/src/tasks/mod.rs
+++ b/src/tasks/mod.rs
@@ -1,3 +1,5 @@
 pub mod block;
+pub mod bundler;
+pub mod oauth;
 pub mod submit;
 pub mod tx_poller;
diff --git a/src/tasks/oauth.rs b/src/tasks/oauth.rs
new file mode 100644
index 0000000..76e0db8
--- /dev/null
+++ b/src/tasks/oauth.rs
@@ -0,0 +1,168 @@
+//! Service responsible for authenticating with the cache with Oauth tokens.
+//! This authenticator periodically fetches a new token every set amount of seconds.
+use std::sync::Arc;
+
+use crate::config::BuilderConfig;
+use oauth2::{
+    basic::{BasicClient, BasicTokenType},
+    reqwest::http_client,
+    AuthUrl, ClientId, ClientSecret, EmptyExtraTokenFields, StandardTokenResponse, TokenUrl,
+};
+use tokio::{sync::RwLock, task::JoinHandle};
+
+const OAUTH_AUDIENCE_CLAIM: &str = "audience";
+
+type Token = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
+
+/// A self-refreshing, periodically fetching authenticator for the block builder.
+/// It is architected as a shareable struct that can be used across all the multiple builder tasks.
+/// It fetches a new token every set amount of seconds, configured through the general builder config.
+/// Readers are guaranteed to not read stale tokens as the [RwLock] guarantees that write tasks (refreshing the token) will claim priority over read access.
+#[derive(Debug, Clone)]
+pub struct Authenticator {
+    pub config: BuilderConfig,
+    inner: Arc<RwLock<AuthenticatorInner>>,
+}
+
+/// Inner state of the Authenticator.
+/// Contains the token that is being used for authentication.
+#[derive(Debug)]
+pub struct AuthenticatorInner {
+    pub token: Option<Token>,
+}
+
+impl Default for AuthenticatorInner {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl AuthenticatorInner {
+    pub fn new() -> Self {
+        Self { token: None }
+    }
+}
+
+impl Authenticator {
+    /// Creates a new Authenticator from the provided builder config.
+    pub fn new(config: &BuilderConfig) -> Self {
+        Self {
+            config: config.clone(),
+            inner: Arc::new(RwLock::new(AuthenticatorInner::new())),
+        }
+    }
+
+    /// Requests a new authentication token and, if successful, sets it to as the token
+    pub async fn authenticate(&self) -> eyre::Result<()> {
+        let token = self.fetch_oauth_token().await?;
+        self.set_token(token).await;
+        Ok(())
+    }
+
+    /// Returns true if there is Some token set
+    pub async fn is_authenticated(&self) -> bool {
+        let lock = self.inner.read().await;
+
+        lock.token.is_some()
+    }
+
+    /// Sets the Authenticator's token to the provided value
+    pub async fn set_token(
+        &self,
+        token: StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
+    ) {
+        let mut lock = self.inner.write().await;
+        lock.token = Some(token);
+    }
+
+    /// Returns the currently set token
+    pub async fn token(&self) -> Option<Token> {
+        let lock = self.inner.read().await;
+        lock.token.clone()
+    }
+
+    /// Fetches an oauth token
+    pub async fn fetch_oauth_token(
+        &self,
+    ) -> eyre::Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>> {
+        let config = self.config.clone();
+
+        let client = BasicClient::new(
+            ClientId::new(config.oauth_client_id.clone()),
+            Some(ClientSecret::new(config.oauth_client_secret.clone())),
+            AuthUrl::new(config.oauth_authenticate_url.clone())?,
+            Some(TokenUrl::new(config.oauth_token_url.clone())?),
+        );
+
+        let token_result = client
+            .exchange_client_credentials()
+            .add_extra_param(OAUTH_AUDIENCE_CLAIM, config.oauth_audience.clone())
+            .request(http_client)?;
+
+        Ok(token_result)
+    }
+
+    /// Spawns a task that periodically fetches a new token every 300 seconds.
+    pub async fn spawn(self) -> JoinHandle<()> {
+        let interval = self.config.oauth_token_refresh_interval;
+
+        tokio::spawn(async move {
+            loop {
+                tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await;
+                tracing::info!("Refreshing oauth token");
+                self.authenticate().await.unwrap();
+            }
+        })
+    }
+}
+
+mod tests {
+    use crate::{config::BuilderConfig, tasks::block::BlockBuilder};
+    use alloy_primitives::Address;
+    use eyre::Result;
+
+    #[ignore = "integration test"]
+    #[tokio::test]
+    async fn test_authenticator() -> Result<()> {
+        use super::*;
+        use oauth2::TokenResponse;
+
+        let config = setup_test_builder()?.1;
+        let auth = Authenticator::new(&config);
+        let token = auth.fetch_oauth_token().await?;
+        dbg!(&token);
+        let token = auth.token().await.unwrap();
+        println!("{:?}", token);
+        assert!(!token.access_token().secret().is_empty());
+        Ok(())
+    }
+
+    #[allow(dead_code)]
+    pub fn setup_test_builder() -> Result<(BlockBuilder, BuilderConfig)> {
+        let config = BuilderConfig {
+            host_chain_id: 17000,
+            ru_chain_id: 17001,
+            host_rpc_url: "http://rpc.holesky.signet.sh".into(),
+            zenith_address: Address::default(),
+            quincey_url: "http://localhost:8080".into(),
+            builder_port: 8080,
+            sequencer_key: None,
+            builder_key: "0000000000000000000000000000000000000000000000000000000000000000".into(),
+            incoming_transactions_buffer: 1,
+            block_confirmation_buffer: 1,
+            builder_rewards_address: Address::default(),
+            rollup_block_gas_limit: 100_000,
+            tx_pool_url: "http://localhost:9000/".into(),
+            tx_pool_cache_duration: 5,
+            tx_pool_poll_interval: 5,
+            oauth_client_id: "some_client_id".into(),
+            oauth_client_secret: "some_client_secret".into(),
+            oauth_authenticate_url: "http://localhost:9000".into(),
+            oauth_token_url: "http://localhost:9000".into(),
+            oauth_audience: "https://transactions.holesky.signet.sh".into(),
+            tx_broadcast_urls: vec!["http://localhost:9000".into()],
+            oauth_token_refresh_interval: 300, // 5 minutes
+        };
+        Ok((BlockBuilder::new(&config), config))
+    }
+}
diff --git a/src/tasks/submit.rs b/src/tasks/submit.rs
index 386feba..675b193 100644
--- a/src/tasks/submit.rs
+++ b/src/tasks/submit.rs
@@ -17,10 +17,7 @@ use alloy::{
 use alloy_primitives::{FixedBytes, U256};
 use alloy_sol_types::SolError;
 use eyre::{bail, eyre};
-use oauth2::{
-    basic::BasicClient, basic::BasicTokenType, reqwest::http_client, AuthUrl, ClientId,
-    ClientSecret, EmptyExtraTokenFields, StandardTokenResponse, TokenResponse, TokenUrl,
-};
+use oauth2::TokenResponse;
 use tokio::{sync::mpsc, task::JoinHandle};
 use tracing::{debug, error, instrument, trace};
 use zenith_types::{
@@ -42,8 +39,7 @@ macro_rules! spawn_provider_send {
     };
 }
 
-/// OAuth Audience Claim Name, required param by IdP for client credential grant
-const OAUTH_AUDIENCE_CLAIM: &str = "audience";
+use super::oauth::Authenticator;
 
 pub enum ControlFlow {
     Retry,
@@ -55,18 +51,16 @@ pub enum ControlFlow {
 pub struct SubmitTask {
     /// Ethereum Provider
     pub provider: Provider,
-
-    /// Zenity
+    /// Zenith
     pub zenith: ZenithInstance,
-
     /// Reqwest
     pub client: reqwest::Client,
-
     /// Sequencer Signer
     pub sequencer_signer: Option<LocalOrAws>,
-
     /// Config
     pub config: crate::config::BuilderConfig,
+    /// Authenticator
+    pub authenticator: Authenticator,
 }
 
 impl SubmitTask {
@@ -77,7 +71,7 @@ impl SubmitTask {
             "pinging quincey for signature"
         );
 
-        let token = self.fetch_oauth_token().await?;
+        let token = self.authenticator.fetch_oauth_token().await?;
 
         let resp: reqwest::Response = self
             .client
@@ -96,24 +90,8 @@ impl SubmitTask {
         serde_json::from_slice(&body).map_err(Into::into)
     }
 
-    async fn fetch_oauth_token(
-        &self,
-    ) -> eyre::Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>> {
-        let client = BasicClient::new(
-            ClientId::new(self.config.oauth_client_id.clone()),
-            Some(ClientSecret::new(self.config.oauth_client_secret.clone())),
-            AuthUrl::new(self.config.oauth_authenticate_url.clone())?,
-            Some(TokenUrl::new(self.config.oauth_token_url.clone())?),
-        );
-
-        let token_result = client
-            .exchange_client_credentials()
-            .add_extra_param(OAUTH_AUDIENCE_CLAIM, self.config.oauth_audience.clone())
-            .request(http_client)?;
-
-        Ok(token_result)
-    }
-
+    /// Constructs the signing request from the in-progress block passed to it and assigns the
+    /// correct height, chain ID, gas limit, and rollup reward address.
     #[instrument(skip_all)]
     async fn construct_sig_request(&self, contents: &InProgressBlock) -> eyre::Result<SignRequest> {
         let ru_chain_id = U256::from(self.config.ru_chain_id);
@@ -129,6 +107,7 @@ impl SubmitTask {
         })
     }
 
+    /// Builds blob transaction from the provided header and signature values
     fn build_blob_tx(
         &self,
         header: Zenith::BlockHeader,
@@ -160,6 +139,7 @@ impl SubmitTask {
         Ok(next)
     }
 
+    /// Submits the EIP 4844 transaction to the network
     async fn submit_transaction(
         &self,
         resp: &SignResponse,
@@ -293,7 +273,7 @@ impl SubmitTask {
         self.submit_transaction(&signed, in_progress).await
     }
 
-    /// Spawn the task.
+    /// Spawns the in progress block building task
     pub fn spawn(self) -> (mpsc::UnboundedSender<InProgressBlock>, JoinHandle<()>) {
         let (sender, mut inbound) = mpsc::unbounded_channel();
         let handle = tokio::spawn(async move {
diff --git a/src/tasks/tx_poller.rs b/src/tasks/tx_poller.rs
index c1809fc..b8b08de 100644
--- a/src/tasks/tx_poller.rs
+++ b/src/tasks/tx_poller.rs
@@ -41,7 +41,7 @@ impl TxPoller {
 
     /// polls the tx-pool for unique transactions and evicts expired transactions.
     /// unique transactions that haven't been seen before are sent into the builder pipeline.
-    pub async fn check_tx_pool(&mut self) -> Result<Vec<TxEnvelope>, Error> {
+    pub async fn check_tx_cache(&mut self) -> Result<Vec<TxEnvelope>, Error> {
         let mut unique: Vec<TxEnvelope> = Vec::new();
 
         let url: Url = Url::parse(&self.config.tx_pool_url)?.join("transactions")?;
@@ -49,14 +49,14 @@ impl TxPoller {
         let response: TxPoolResponse = from_slice(result.text().await?.as_bytes())?;
 
         response.transactions.iter().for_each(|entry| {
-            self.check_cache(entry.clone(), &mut unique);
+            self.check_seen_txs(entry.clone(), &mut unique);
         });
 
         Ok(unique)
     }
 
     /// checks if the transaction has been seen before and if not, adds it to the unique transactions list.
-    fn check_cache(&mut self, tx: TxEnvelope, unique: &mut Vec<TxEnvelope>) {
+    fn check_seen_txs(&mut self, tx: TxEnvelope, unique: &mut Vec<TxEnvelope>) {
         self.seen_txns.entry(*tx.tx_hash()).or_insert_with(|| {
             // add to unique transactions
             unique.push(tx.clone());
@@ -89,7 +89,7 @@ impl TxPoller {
         let handle: JoinHandle<()> = tokio::spawn(async move {
             loop {
                 let channel = tx_channel.clone();
-                let txns = self.check_tx_pool().await;
+                let txns = self.check_tx_cache().await;
 
                 // send recently discovered transactions to the builder pipeline
                 match txns {
diff --git a/tests/bundle_poller_test.rs b/tests/bundle_poller_test.rs
new file mode 100644
index 0000000..8c7769d
--- /dev/null
+++ b/tests/bundle_poller_test.rs
@@ -0,0 +1,50 @@
+mod tests {
+    use alloy_primitives::Address;
+    use builder::{
+        config::BuilderConfig,
+        tasks::{block::BlockBuilder, oauth::Authenticator},
+    };
+    use eyre::Result;
+
+    #[ignore = "integration test"]
+    #[tokio::test]
+    async fn test_bundle_poller_roundtrip() -> Result<()> {
+        let (_, config) = setup_test_builder().await.unwrap();
+        let auth = Authenticator::new(&config);
+        let mut bundle_poller = builder::tasks::bundler::BundlePoller::new(&config, auth).await;
+
+        let got = bundle_poller.check_bundle_cache().await?;
+        dbg!(got);
+
+        Ok(())
+    }
+
+    async fn setup_test_builder() -> Result<(BlockBuilder, BuilderConfig)> {
+        let config = BuilderConfig {
+            host_chain_id: 17000,
+            ru_chain_id: 17001,
+            host_rpc_url: "http://rpc.holesky.signet.sh".into(),
+            zenith_address: Address::default(),
+            quincey_url: "http://localhost:8080".into(),
+            builder_port: 8080,
+            sequencer_key: None,
+            builder_key: "0000000000000000000000000000000000000000000000000000000000000000".into(),
+            incoming_transactions_buffer: 1,
+            block_confirmation_buffer: 1,
+            builder_rewards_address: Address::default(),
+            rollup_block_gas_limit: 100_000,
+            tx_pool_url: "http://localhost:9000/".into(),
+            // tx_pool_url: "https://transactions.holesky.signet.sh".into(),
+            tx_pool_cache_duration: 5,
+            tx_pool_poll_interval: 5,
+            oauth_client_id: "some_client_id".into(),
+            oauth_client_secret: "some_client_secret".into(),
+            oauth_authenticate_url: "http://localhost:8080".into(),
+            oauth_token_url: "http://localhost:8080".into(),
+            oauth_audience: "https://transactions.holesky.signet.sh".into(),
+            tx_broadcast_urls: vec!["http://localhost:9000".into()],
+            oauth_token_refresh_interval: 300, // 5 minutes
+        };
+        Ok((BlockBuilder::new(&config), config))
+    }
+}
diff --git a/tests/tx_poller_test.rs b/tests/tx_poller_test.rs
index 11e8c88..2bc6a2c 100644
--- a/tests/tx_poller_test.rs
+++ b/tests/tx_poller_test.rs
@@ -6,7 +6,7 @@ mod tests {
     use alloy_primitives::{bytes, Address, TxKind, U256};
     use builder::config::BuilderConfig;
     use builder::tasks::{block::BlockBuilder, tx_poller};
-    use eyre::Result;
+    use eyre::{Ok, Result};
 
     #[ignore = "integration test"]
     #[tokio::test]
@@ -21,7 +21,7 @@ mod tests {
         let mut poller = tx_poller::TxPoller::new(&config);
 
         // Fetch transactions the pool
-        let transactions = poller.check_tx_pool().await?;
+        let transactions = poller.check_tx_cache().await?;
 
         // Ensure at least one transaction exists
         assert!(!transactions.is_empty());
@@ -63,7 +63,7 @@ mod tests {
     }
 
     // Sets up a block builder with test values
-    async fn setup_test_builder() -> Result<(BlockBuilder, BuilderConfig)> {
+    pub async fn setup_test_builder() -> Result<(BlockBuilder, BuilderConfig)> {
         let config = BuilderConfig {
             host_chain_id: 17000,
             ru_chain_id: 17001,
@@ -86,6 +86,7 @@ mod tests {
             oauth_authenticate_url: "http://localhost:8080".into(),
             oauth_token_url: "http://localhost:8080".into(),
             oauth_audience: "https://transactions.holesky.signet.sh".into(),
+            oauth_token_refresh_interval: 300, // 5 minutes
         };
         Ok((BlockBuilder::new(&config), config))
     }
