diff --git a/Cargo.lock b/Cargo.lock
index c5cee452b1ae..fb5be136e10e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10784,15 +10784,18 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "axum 0.7.7",
- "serde",
+ "http-body-util",
  "serde_json",
+ "test-casing",
  "tokio",
+ "tower 0.4.13",
  "tower-http",
  "tracing",
  "vise",
- "zksync_config",
  "zksync_dal",
+ "zksync_node_test_utils",
  "zksync_types",
+ "zksync_utils",
 ]
 
 [[package]]
@@ -10800,16 +10803,13 @@ name = "zksync_contract_verifier"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "ctrlc",
- "futures 0.3.31",
- "structopt",
+ "clap 4.5.20",
  "tokio",
  "tracing",
  "zksync_config",
  "zksync_contract_verifier_lib",
  "zksync_core_leftovers",
  "zksync_dal",
- "zksync_env_config",
  "zksync_queued_job_processor",
  "zksync_utils",
  "zksync_vlog",
diff --git a/Cargo.toml b/Cargo.toml
index 87e0de13129f..e491c64605bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -130,6 +130,7 @@ google-cloud-storage = "0.20.0"
 governor = "0.4.2"
 hex = "0.4"
 http = "1.1"
+http-body-util = "0.1.2"
 httpmock = "0.7.0"
 hyper = "1.3"
 insta = "1.29.0"
diff --git a/core/bin/contract-verifier/Cargo.toml b/core/bin/contract-verifier/Cargo.toml
index f088c2337e71..5e9a9efc6e7e 100644
--- a/core/bin/contract-verifier/Cargo.toml
+++ b/core/bin/contract-verifier/Cargo.toml
@@ -12,7 +12,6 @@ publish = false
 
 [dependencies]
 zksync_dal.workspace = true
-zksync_env_config.workspace = true
 zksync_config = { workspace = true, features = ["observability_ext"] }
 zksync_contract_verifier_lib.workspace = true
 zksync_queued_job_processor.workspace = true
@@ -21,8 +20,6 @@ zksync_vlog.workspace = true
 zksync_core_leftovers.workspace = true
 
 anyhow.workspace = true
+clap = { workspace = true, features = ["derive"] }
 tokio = { workspace = true, features = ["full"] }
-futures.workspace = true
-ctrlc.workspace = true
-structopt.workspace = true
 tracing.workspace = true
diff --git a/core/bin/contract-verifier/src/main.rs b/core/bin/contract-verifier/src/main.rs
index 6929f8bfe04d..88f25256c40d 100644
--- a/core/bin/contract-verifier/src/main.rs
+++ b/core/bin/contract-verifier/src/main.rs
@@ -1,8 +1,7 @@
-use std::{cell::RefCell, time::Duration};
+use std::{path::PathBuf, time::Duration};
 
-use anyhow::Context;
-use futures::{channel::mpsc, executor::block_on, SinkExt, StreamExt};
-use structopt::StructOpt;
+use anyhow::Context as _;
+use clap::Parser;
 use tokio::sync::watch;
 use zksync_config::configs::PrometheusConfig;
 use zksync_contract_verifier_lib::ContractVerifier;
@@ -12,27 +11,31 @@ use zksync_queued_job_processor::JobProcessor;
 use zksync_utils::wait_for_tasks::ManagedTasks;
 use zksync_vlog::prometheus::PrometheusExporterConfig;
 
-#[derive(StructOpt)]
-#[structopt(name = "ZKsync contract code verifier", author = "Matter Labs")]
+#[derive(Debug, Parser)]
+#[command(name = "ZKsync contract code verifier", author = "Matter Labs")]
 struct Opt {
     /// Number of jobs to process. If None, runs indefinitely.
-    #[structopt(long)]
+    #[arg(long)]
     jobs_number: Option<usize>,
     /// Path to the configuration file.
-    #[structopt(long)]
-    config_path: Option<std::path::PathBuf>,
+    #[arg(long)]
+    config_path: Option<PathBuf>,
     /// Path to the secrets file.
-    #[structopt(long)]
-    secrets_path: Option<std::path::PathBuf>,
+    #[arg(long)]
+    secrets_path: Option<PathBuf>,
 }
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
-    let opt = Opt::from_args();
+    let opt = Opt::parse();
 
     let general_config = load_general_config(opt.config_path).context("general config")?;
-    let database_secrets = load_database_secrets(opt.secrets_path).context("database secrets")?;
+    let observability_config = general_config
+        .observability
+        .context("ObservabilityConfig")?;
+    let _observability_guard = observability_config.install()?;
 
+    let database_secrets = load_database_secrets(opt.secrets_path).context("database secrets")?;
     let verifier_config = general_config
         .contract_verifier
         .context("ContractVerifierConfig")?;
@@ -46,33 +49,13 @@ async fn main() -> anyhow::Result<()> {
             .context("Master DB URL is absent")?,
     )
     .build()
-    .await
-    .unwrap();
-
-    let observability_config = general_config
-        .observability
-        .context("ObservabilityConfig")?;
-
-    let _observability_guard = observability_config.install()?;
+    .await?;
 
     let (stop_sender, stop_receiver) = watch::channel(false);
-    let (stop_signal_sender, mut stop_signal_receiver) = mpsc::channel(256);
-    {
-        let stop_signal_sender = RefCell::new(stop_signal_sender.clone());
-        ctrlc::set_handler(move || {
-            let mut sender = stop_signal_sender.borrow_mut();
-            block_on(sender.send(true)).expect("Ctrl+C signal send");
-        })
-        .expect("Error setting Ctrl+C handler");
-    }
-
     let contract_verifier = ContractVerifier::new(verifier_config.compilation_timeout(), pool)
         .await
         .context("failed initializing contract verifier")?;
     let tasks = vec![
-        // TODO PLA-335: Leftovers after the prover DB split.
-        // The prover connection pool is not used by the contract verifier, but we need to pass it
-        // since `JobProcessor` trait requires it.
         tokio::spawn(contract_verifier.run(stop_receiver.clone(), opt.jobs_number)),
         tokio::spawn(
             PrometheusExporterConfig::pull(prometheus_config.listener_port).run(stop_receiver),
@@ -82,7 +65,7 @@ async fn main() -> anyhow::Result<()> {
     let mut tasks = ManagedTasks::new(tasks);
     tokio::select! {
         () = tasks.wait_single() => {},
-        _ = stop_signal_receiver.next() => {
+        _ = tokio::signal::ctrl_c() => {
             tracing::info!("Stop signal received, shutting down");
         },
     };
diff --git a/core/lib/contract_verifier/src/lib.rs b/core/lib/contract_verifier/src/lib.rs
index 425440fa2eb6..686bb0d7bdc3 100644
--- a/core/lib/contract_verifier/src/lib.rs
+++ b/core/lib/contract_verifier/src/lib.rs
@@ -14,7 +14,7 @@ use zksync_dal::{contract_verification_dal::DeployedContractData, ConnectionPool
 use zksync_queued_job_processor::{async_trait, JobProcessor};
 use zksync_types::{
     contract_verification_api::{
-        CompilationArtifacts, CompilerType, VerificationIncomingRequest, VerificationInfo,
+        self as api, CompilationArtifacts, VerificationIncomingRequest, VerificationInfo,
         VerificationRequest,
     },
     Address, CONTRACT_DEPLOYER_ADDRESS,
@@ -35,6 +35,65 @@ mod resolver;
 #[cfg(test)]
 mod tests;
 
+#[derive(Debug)]
+struct ZkCompilerVersions {
+    /// Version of the base / non-ZK compiler.
+    pub base: String,
+    /// Version of the ZK compiler.
+    pub zk: String,
+}
+
+/// Internal counterpart of `ContractVersions` from API that encompasses all supported compilation modes.
+#[derive(Debug)]
+enum VersionedCompiler {
+    Solc(String),
+    #[allow(dead_code)] // TODO (EVM-864): add vyper support
+    Vyper(String),
+    ZkSolc(ZkCompilerVersions),
+    ZkVyper(ZkCompilerVersions),
+}
+
+impl From<api::CompilerVersions> for VersionedCompiler {
+    fn from(versions: api::CompilerVersions) -> Self {
+        match versions {
+            api::CompilerVersions::Solc {
+                compiler_solc_version,
+                compiler_zksolc_version: None,
+            } => Self::Solc(compiler_solc_version),
+
+            api::CompilerVersions::Solc {
+                compiler_solc_version,
+                compiler_zksolc_version: Some(zk),
+            } => Self::ZkSolc(ZkCompilerVersions {
+                base: compiler_solc_version,
+                zk,
+            }),
+
+            api::CompilerVersions::Vyper {
+                compiler_vyper_version,
+                compiler_zkvyper_version: None,
+            } => Self::Vyper(compiler_vyper_version),
+
+            api::CompilerVersions::Vyper {
+                compiler_vyper_version,
+                compiler_zkvyper_version: Some(zk),
+            } => Self::ZkVyper(ZkCompilerVersions {
+                base: compiler_vyper_version,
+                zk,
+            }),
+        }
+    }
+}
+
+impl VersionedCompiler {
+    fn expected_bytecode_kind(&self) -> BytecodeMarker {
+        match self {
+            Self::Solc(_) | Self::Vyper(_) => BytecodeMarker::Evm,
+            Self::ZkSolc(_) | Self::ZkVyper(_) => BytecodeMarker::EraVm,
+        }
+    }
+}
+
 enum ConstructorArgs {
     Check(Vec<u8>),
     Ignore,
@@ -112,19 +171,19 @@ impl ContractVerifier {
         let mut transaction = storage.start_transaction().await?;
         transaction
             .contract_verification_dal()
-            .set_zksolc_versions(supported_versions.zksolc)
+            .set_zksolc_versions(&supported_versions.zksolc)
             .await?;
         transaction
             .contract_verification_dal()
-            .set_solc_versions(supported_versions.solc)
+            .set_solc_versions(&supported_versions.solc)
             .await?;
         transaction
             .contract_verification_dal()
-            .set_zkvyper_versions(supported_versions.zkvyper)
+            .set_zkvyper_versions(&supported_versions.zkvyper)
             .await?;
         transaction
             .contract_verification_dal()
-            .set_vyper_versions(supported_versions.vyper)
+            .set_vyper_versions(&supported_versions.vyper)
             .await?;
         transaction.commit().await?;
         Ok(())
@@ -214,13 +273,11 @@ impl ContractVerifier {
 
     async fn compile_zksolc(
         &self,
+        version: &ZkCompilerVersions,
         req: VerificationIncomingRequest,
     ) -> Result<CompilationArtifacts, ContractVerifierError> {
-        let zksolc = self
-            .compiler_resolver
-            .resolve_zksolc(&req.compiler_versions)
-            .await?;
-        tracing::debug!(?zksolc, ?req.compiler_versions, "resolved compiler");
+        let zksolc = self.compiler_resolver.resolve_zksolc(version).await?;
+        tracing::debug!(?zksolc, ?version, "resolved compiler");
         let input = ZkSolc::build_input(req)?;
 
         time::timeout(self.compilation_timeout, zksolc.compile(input))
@@ -230,13 +287,11 @@ impl ContractVerifier {
 
     async fn compile_zkvyper(
         &self,
+        version: &ZkCompilerVersions,
         req: VerificationIncomingRequest,
     ) -> Result<CompilationArtifacts, ContractVerifierError> {
-        let zkvyper = self
-            .compiler_resolver
-            .resolve_zkvyper(&req.compiler_versions)
-            .await?;
-        tracing::debug!(?zkvyper, ?req.compiler_versions, "resolved compiler");
+        let zkvyper = self.compiler_resolver.resolve_zkvyper(version).await?;
+        tracing::debug!(?zkvyper, ?version, "resolved compiler");
         let input = ZkVyper::build_input(req)?;
         time::timeout(self.compilation_timeout, zkvyper.compile(input))
             .await
@@ -245,12 +300,10 @@ impl ContractVerifier {
 
     async fn compile_solc(
         &self,
+        version: &str,
         req: VerificationIncomingRequest,
     ) -> Result<CompilationArtifacts, ContractVerifierError> {
-        let solc = self
-            .compiler_resolver
-            .resolve_solc(req.compiler_versions.compiler_version())
-            .await?;
+        let solc = self.compiler_resolver.resolve_solc(version).await?;
         tracing::debug!(?solc, ?req.compiler_versions, "resolved compiler");
         let input = Solc::build_input(req)?;
 
@@ -276,15 +329,24 @@ impl ContractVerifier {
             return Err(err.into());
         }
 
-        match (bytecode_marker, compiler_type) {
-            (BytecodeMarker::EraVm, CompilerType::Solc) => self.compile_zksolc(req).await,
-            (BytecodeMarker::EraVm, CompilerType::Vyper) => self.compile_zkvyper(req).await,
-            (BytecodeMarker::Evm, CompilerType::Solc) => self.compile_solc(req).await,
-            (BytecodeMarker::Evm, CompilerType::Vyper) => {
-                // TODO: add vyper support
+        let compiler = VersionedCompiler::from(req.compiler_versions.clone());
+        if compiler.expected_bytecode_kind() != bytecode_marker {
+            let err = anyhow::anyhow!(
+                "bytecode kind expected by compiler {compiler:?} differs from the actual bytecode kind \
+                 of the verified contract ({bytecode_marker:?})",
+            );
+            return Err(err.into());
+        }
+
+        match &compiler {
+            VersionedCompiler::Solc(version) => self.compile_solc(version, req).await,
+            VersionedCompiler::Vyper(_) => {
+                // TODO (EVM-864): add vyper support
                 let err = anyhow::anyhow!("vyper toolchain is not yet supported for EVM contracts");
                 return Err(err.into());
             }
+            VersionedCompiler::ZkSolc(version) => self.compile_zksolc(version, req).await,
+            VersionedCompiler::ZkVyper(version) => self.compile_zkvyper(version, req).await,
         }
     }
 
diff --git a/core/lib/contract_verifier/src/resolver.rs b/core/lib/contract_verifier/src/resolver.rs
index 347db8fff094..34a70b759797 100644
--- a/core/lib/contract_verifier/src/resolver.rs
+++ b/core/lib/contract_verifier/src/resolver.rs
@@ -6,12 +6,13 @@ use std::{
 use anyhow::Context as _;
 use tokio::fs;
 use zksync_queued_job_processor::async_trait;
-use zksync_types::contract_verification_api::{CompilationArtifacts, CompilerVersions};
+use zksync_types::contract_verification_api::CompilationArtifacts;
 use zksync_utils::env::Workspace;
 
 use crate::{
     compilers::{Solc, SolcInput, ZkSolc, ZkSolcInput, ZkVyper, ZkVyperInput},
     error::ContractVerifierError,
+    ZkCompilerVersions,
 };
 
 #[derive(Debug, Clone, Copy)]
@@ -111,13 +112,13 @@ pub(crate) trait CompilerResolver: fmt::Debug + Send + Sync {
     /// Resolves a `zksolc` compiler.
     async fn resolve_zksolc(
         &self,
-        versions: &CompilerVersions,
+        version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkSolcInput>>, ContractVerifierError>;
 
     /// Resolves a `zkvyper` compiler.
     async fn resolve_zkvyper(
         &self,
-        versions: &CompilerVersions,
+        version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkVyperInput>>, ContractVerifierError>;
 }
 
@@ -198,14 +199,14 @@ impl CompilerResolver for EnvCompilerResolver {
 
     async fn resolve_zksolc(
         &self,
-        versions: &CompilerVersions,
+        version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkSolcInput>>, ContractVerifierError> {
-        let zksolc_version = versions.zk_compiler_version();
+        let zksolc_version = &version.zk;
         let zksolc_path = CompilerType::ZkSolc
             .bin_path(&self.home_dir, zksolc_version)
             .await?;
         let solc_path = CompilerType::Solc
-            .bin_path(&self.home_dir, versions.compiler_version())
+            .bin_path(&self.home_dir, &version.base)
             .await?;
         let compiler_paths = CompilerPaths {
             base: solc_path,
@@ -219,13 +220,13 @@ impl CompilerResolver for EnvCompilerResolver {
 
     async fn resolve_zkvyper(
         &self,
-        versions: &CompilerVersions,
+        version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkVyperInput>>, ContractVerifierError> {
         let zkvyper_path = CompilerType::ZkVyper
-            .bin_path(&self.home_dir, versions.zk_compiler_version())
+            .bin_path(&self.home_dir, &version.zk)
             .await?;
         let vyper_path = CompilerType::Vyper
-            .bin_path(&self.home_dir, versions.compiler_version())
+            .bin_path(&self.home_dir, &version.base)
             .await?;
         let compiler_paths = CompilerPaths {
             base: vyper_path,
diff --git a/core/lib/contract_verifier/src/tests/mod.rs b/core/lib/contract_verifier/src/tests/mod.rs
index f05d3155a6d4..7caa5f32c991 100644
--- a/core/lib/contract_verifier/src/tests/mod.rs
+++ b/core/lib/contract_verifier/src/tests/mod.rs
@@ -280,18 +280,18 @@ impl CompilerResolver for MockCompilerResolver {
 
     async fn resolve_zksolc(
         &self,
-        versions: &CompilerVersions,
+        version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkSolcInput>>, ContractVerifierError> {
-        if versions.compiler_version() != SOLC_VERSION {
+        if version.base != SOLC_VERSION {
             return Err(ContractVerifierError::UnknownCompilerVersion(
                 "solc",
-                versions.compiler_version().to_owned(),
+                version.base.clone(),
             ));
         }
-        if versions.zk_compiler_version() != ZKSOLC_VERSION {
+        if version.zk != ZKSOLC_VERSION {
             return Err(ContractVerifierError::UnknownCompilerVersion(
                 "zksolc",
-                versions.zk_compiler_version().to_owned(),
+                version.zk.clone(),
             ));
         }
         Ok(Box::new(self.clone()))
@@ -299,7 +299,7 @@ impl CompilerResolver for MockCompilerResolver {
 
     async fn resolve_zkvyper(
         &self,
-        _versions: &CompilerVersions,
+        _version: &ZkCompilerVersions,
     ) -> Result<Box<dyn Compiler<ZkVyperInput>>, ContractVerifierError> {
         unreachable!("not tested")
     }
@@ -311,7 +311,7 @@ fn test_request(address: Address, source: &str) -> VerificationIncomingRequest {
         source_code_data: SourceCodeData::SolSingleFile(source.into()),
         contract_name: "Counter".to_owned(),
         compiler_versions: CompilerVersions::Solc {
-            compiler_zksolc_version: ZKSOLC_VERSION.to_owned(),
+            compiler_zksolc_version: Some(ZKSOLC_VERSION.to_owned()),
             compiler_solc_version: SOLC_VERSION.to_owned(),
         },
         optimization_used: true,
@@ -375,7 +375,7 @@ async fn contract_verifier_basics(contract: TestContract) {
     req.constructor_arguments = ethabi::encode(contract.constructor_args()).into();
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -468,10 +468,14 @@ async fn verifying_evm_bytecode(contract: TestContract) {
     )
     .await;
     let mut req = test_request(address, contract.source());
+    req.compiler_versions = CompilerVersions::Solc {
+        compiler_solc_version: SOLC_VERSION.to_owned(),
+        compiler_zksolc_version: None,
+    };
     req.constructor_arguments = ethabi::encode(contract.constructor_args()).into();
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -513,7 +517,7 @@ async fn bytecode_mismatch_error() {
     let req = test_request(address, COUNTER_CONTRACT);
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -578,6 +582,13 @@ async fn args_mismatch_error(contract: TestContract, bytecode_kind: BytecodeMark
     }
 
     let mut req = test_request(address, contract.source());
+    if matches!(bytecode_kind, BytecodeMarker::Evm) {
+        req.compiler_versions = CompilerVersions::Solc {
+            compiler_zksolc_version: None,
+            compiler_solc_version: SOLC_VERSION.to_owned(),
+        };
+    }
+
     // Intentionally encode incorrect constructor args
     req.constructor_arguments = match contract {
         TestContract::Counter => ethabi::encode(&[Token::Bool(true)]).into(),
@@ -585,7 +596,7 @@ async fn args_mismatch_error(contract: TestContract, bytecode_kind: BytecodeMark
     };
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -648,10 +659,14 @@ async fn creation_bytecode_mismatch() {
         &[],
     )
     .await;
-    let req = test_request(address, COUNTER_CONTRACT);
+    let mut req = test_request(address, COUNTER_CONTRACT);
+    req.compiler_versions = CompilerVersions::Solc {
+        compiler_zksolc_version: None,
+        compiler_solc_version: SOLC_VERSION.to_owned(),
+    };
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -696,14 +711,14 @@ async fn no_compiler_version() {
     mock_deployment(&mut storage, address, vec![0xff; 32], &[]).await;
     let req = VerificationIncomingRequest {
         compiler_versions: CompilerVersions::Solc {
-            compiler_zksolc_version: ZKSOLC_VERSION.to_owned(),
+            compiler_zksolc_version: Some(ZKSOLC_VERSION.to_owned()),
             compiler_solc_version: "1.0.0".to_owned(), // a man can dream
         },
         ..test_request(address, COUNTER_CONTRACT)
     };
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
diff --git a/core/lib/contract_verifier/src/tests/real.rs b/core/lib/contract_verifier/src/tests/real.rs
index 5f550a5feea8..a7113044b405 100644
--- a/core/lib/contract_verifier/src/tests/real.rs
+++ b/core/lib/contract_verifier/src/tests/real.rs
@@ -8,7 +8,7 @@ use zksync_utils::bytecode::validate_bytecode;
 
 use super::*;
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 struct TestCompilerVersions {
     solc: String,
     zksolc: String,
@@ -26,10 +26,20 @@ impl TestCompilerVersions {
         })
     }
 
-    fn for_zksolc(self) -> CompilerVersions {
+    fn zksolc(self) -> ZkCompilerVersions {
+        ZkCompilerVersions {
+            base: self.solc,
+            zk: self.zksolc,
+        }
+    }
+
+    fn solc_for_api(self, bytecode_kind: BytecodeMarker) -> CompilerVersions {
         CompilerVersions::Solc {
             compiler_solc_version: self.solc,
-            compiler_zksolc_version: self.zksolc,
+            compiler_zksolc_version: match bytecode_kind {
+                BytecodeMarker::Evm => None,
+                BytecodeMarker::EraVm => Some(self.zksolc),
+            },
         }
     }
 }
@@ -70,10 +80,12 @@ macro_rules! real_resolver {
 async fn using_real_compiler() {
     let (compiler_resolver, supported_compilers) = real_resolver!();
 
-    let versions = supported_compilers.for_zksolc();
-    let compiler = compiler_resolver.resolve_zksolc(&versions).await.unwrap();
+    let compiler = compiler_resolver
+        .resolve_zksolc(&supported_compilers.clone().zksolc())
+        .await
+        .unwrap();
     let req = VerificationIncomingRequest {
-        compiler_versions: versions,
+        compiler_versions: supported_compilers.solc_for_api(BytecodeMarker::EraVm),
         ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT)
     };
     let input = ZkSolc::build_input(req).unwrap();
@@ -92,7 +104,7 @@ async fn using_standalone_solc() {
     let req = VerificationIncomingRequest {
         compiler_versions: CompilerVersions::Solc {
             compiler_solc_version: version.clone(),
-            compiler_zksolc_version: "1000.0.0".to_owned(), // not used
+            compiler_zksolc_version: None,
         },
         ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT)
     };
@@ -108,23 +120,22 @@ async fn using_standalone_solc() {
 async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) {
     let (compiler_resolver, supported_compilers) = real_resolver!();
 
-    let versions = supported_compilers.for_zksolc();
     let req = VerificationIncomingRequest {
-        compiler_versions: versions,
+        compiler_versions: supported_compilers.clone().solc_for_api(bytecode_kind),
         ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT)
     };
     let address = Address::repeat_byte(1);
     let output = match bytecode_kind {
         BytecodeMarker::EraVm => {
             let compiler = compiler_resolver
-                .resolve_zksolc(&req.compiler_versions)
+                .resolve_zksolc(&supported_compilers.zksolc())
                 .await
                 .unwrap();
             let input = ZkSolc::build_input(req.clone()).unwrap();
             compiler.compile(input).await.unwrap()
         }
         BytecodeMarker::Evm => {
-            let solc_version = req.compiler_versions.compiler_version();
+            let solc_version = &supported_compilers.solc;
             let compiler = compiler_resolver.resolve_solc(solc_version).await.unwrap();
             let input = Solc::build_input(req.clone()).unwrap();
             compiler.compile(input).await.unwrap()
@@ -151,7 +162,7 @@ async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) {
     }
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
@@ -174,10 +185,9 @@ async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) {
 async fn compilation_errors(bytecode_kind: BytecodeMarker) {
     let (compiler_resolver, supported_compilers) = real_resolver!();
 
-    let versions = supported_compilers.for_zksolc();
     let address = Address::repeat_byte(1);
     let req = VerificationIncomingRequest {
-        compiler_versions: versions,
+        compiler_versions: supported_compilers.solc_for_api(bytecode_kind),
         source_code_data: SourceCodeData::SolSingleFile("contract ???".to_owned()),
         ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT)
     };
@@ -196,7 +206,7 @@ async fn compilation_errors(bytecode_kind: BytecodeMarker) {
 
     let request_id = storage
         .contract_verification_dal()
-        .add_contract_verification_request(req)
+        .add_contract_verification_request(&req)
         .await
         .unwrap();
 
diff --git a/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json b/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json
index 1d515edba819..0db6ba6f51b6 100644
--- a/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json
+++ b/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json
@@ -69,7 +69,7 @@
       false,
       false,
       false,
-      false,
+      true,
       false,
       false,
       true,
diff --git a/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json b/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json
index ebe8ce232cfb..ac7989a5be77 100644
--- a/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json
+++ b/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json
@@ -67,7 +67,7 @@
       false,
       false,
       false,
-      false,
+      true,
       false,
       false,
       true,
diff --git a/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql
new file mode 100644
index 000000000000..2693a565fd02
--- /dev/null
+++ b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE contract_verification_requests
+  ALTER COLUMN zk_compiler_version SET NOT NULL;
diff --git a/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql
new file mode 100644
index 000000000000..92a689956f55
--- /dev/null
+++ b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE contract_verification_requests
+  ALTER COLUMN zk_compiler_version DROP NOT NULL;
diff --git a/core/lib/dal/src/contract_verification_dal.rs b/core/lib/dal/src/contract_verification_dal.rs
index 1a827545ca13..93a4ce2fd35a 100644
--- a/core/lib/dal/src/contract_verification_dal.rs
+++ b/core/lib/dal/src/contract_verification_dal.rs
@@ -76,7 +76,7 @@ impl ContractVerificationDal<'_, '_> {
 
     pub async fn add_contract_verification_request(
         &mut self,
-        query: VerificationIncomingRequest,
+        query: &VerificationIncomingRequest,
     ) -> DalResult<usize> {
         sqlx::query!(
             r#"
@@ -104,12 +104,12 @@ impl ContractVerificationDal<'_, '_> {
             query.contract_address.as_bytes(),
             // Serialization should always succeed.
             serde_json::to_string(&query.source_code_data).unwrap(),
-            query.contract_name,
+            &query.contract_name,
             query.compiler_versions.zk_compiler_version(),
             query.compiler_versions.compiler_version(),
             query.optimization_used,
-            query.optimizer_mode,
-            query.constructor_arguments.0,
+            query.optimizer_mode.as_deref(),
+            query.constructor_arguments.0.as_slice(),
             query.is_system,
             query.force_evmla,
         )
@@ -441,7 +441,7 @@ impl ContractVerificationDal<'_, '_> {
     async fn set_compiler_versions(
         &mut self,
         compiler: Compiler,
-        versions: Vec<String>,
+        versions: &[String],
     ) -> DalResult<()> {
         let mut transaction = self.storage.start_transaction().await?;
         let compiler = format!("{compiler}");
@@ -472,7 +472,7 @@ impl ContractVerificationDal<'_, '_> {
                 UNNEST($1::TEXT []) AS u (version)
             ON CONFLICT (version, compiler) DO NOTHING
             "#,
-            &versions,
+            versions,
             &compiler,
         )
         .instrument("set_compiler_versions#insert")
@@ -484,20 +484,20 @@ impl ContractVerificationDal<'_, '_> {
         transaction.commit().await
     }
 
-    pub async fn set_zksolc_versions(&mut self, versions: Vec<String>) -> DalResult<()> {
+    pub async fn set_zksolc_versions(&mut self, versions: &[String]) -> DalResult<()> {
         self.set_compiler_versions(Compiler::ZkSolc, versions).await
     }
 
-    pub async fn set_solc_versions(&mut self, versions: Vec<String>) -> DalResult<()> {
+    pub async fn set_solc_versions(&mut self, versions: &[String]) -> DalResult<()> {
         self.set_compiler_versions(Compiler::Solc, versions).await
     }
 
-    pub async fn set_zkvyper_versions(&mut self, versions: Vec<String>) -> DalResult<()> {
+    pub async fn set_zkvyper_versions(&mut self, versions: &[String]) -> DalResult<()> {
         self.set_compiler_versions(Compiler::ZkVyper, versions)
             .await
     }
 
-    pub async fn set_vyper_versions(&mut self, versions: Vec<String>) -> DalResult<()> {
+    pub async fn set_vyper_versions(&mut self, versions: &[String]) -> DalResult<()> {
         self.set_compiler_versions(Compiler::Vyper, versions).await
     }
 
@@ -567,7 +567,9 @@ mod tests {
     use std::collections::HashMap;
 
     use zksync_types::{
-        tx::IncludedTxLocation, Execute, L1BatchNumber, L2BlockNumber, ProtocolVersion,
+        contract_verification_api::{CompilerVersions, SourceCodeData},
+        tx::IncludedTxLocation,
+        Execute, L1BatchNumber, L2BlockNumber, ProtocolVersion,
     };
     use zksync_utils::bytecode::hash_bytecode;
     use zksync_vm_interface::{tracer::ValidationTraces, TransactionExecutionMetrics};
@@ -645,4 +647,66 @@ mod tests {
         assert_eq!(contract.contract_address, Some(CONTRACT_DEPLOYER_ADDRESS));
         assert_eq!(contract.calldata.unwrap(), tx.execute.calldata);
     }
+
+    async fn test_working_with_verification_requests(zksolc: Option<&str>) {
+        let request = VerificationIncomingRequest {
+            contract_address: Address::repeat_byte(11),
+            source_code_data: SourceCodeData::SolSingleFile("contract Test {}".to_owned()),
+            contract_name: "Test".to_string(),
+            compiler_versions: CompilerVersions::Solc {
+                compiler_zksolc_version: zksolc.map(str::to_owned),
+                compiler_solc_version: "0.8.27".to_owned(),
+            },
+            optimization_used: true,
+            optimizer_mode: Some("z".to_owned()),
+            constructor_arguments: web3::Bytes(b"test".to_vec()),
+            is_system: false,
+            force_evmla: true,
+        };
+
+        let pool = ConnectionPool::<Core>::test_pool().await;
+        let mut conn = pool.connection().await.unwrap();
+        let id = conn
+            .contract_verification_dal()
+            .add_contract_verification_request(&request)
+            .await
+            .unwrap();
+
+        let status = conn
+            .contract_verification_dal()
+            .get_verification_request_status(id)
+            .await
+            .unwrap()
+            .expect("request not persisted");
+        assert_eq!(status.status, "queued");
+
+        let req = conn
+            .contract_verification_dal()
+            .get_next_queued_verification_request(Duration::from_secs(600))
+            .await
+            .unwrap()
+            .expect("request not queued");
+        assert_eq!(req.id, id);
+        assert_eq!(req.req.contract_address, request.contract_address);
+        assert_eq!(req.req.contract_name, request.contract_name);
+        assert_eq!(req.req.compiler_versions, request.compiler_versions);
+        assert_eq!(req.req.optimization_used, request.optimization_used);
+        assert_eq!(req.req.optimizer_mode, request.optimizer_mode);
+        assert_eq!(req.req.constructor_arguments, request.constructor_arguments);
+        assert_eq!(req.req.is_system, request.is_system);
+        assert_eq!(req.req.force_evmla, request.force_evmla);
+
+        let maybe_req = conn
+            .contract_verification_dal()
+            .get_next_queued_verification_request(Duration::from_secs(600))
+            .await
+            .unwrap();
+        assert!(maybe_req.is_none());
+    }
+
+    #[tokio::test]
+    async fn working_with_verification_requests() {
+        test_working_with_verification_requests(None).await;
+        test_working_with_verification_requests(Some("1.5.7")).await;
+    }
 }
diff --git a/core/lib/dal/src/models/storage_verification_request.rs b/core/lib/dal/src/models/storage_verification_request.rs
index 61895fab76d3..ae4718e41290 100644
--- a/core/lib/dal/src/models/storage_verification_request.rs
+++ b/core/lib/dal/src/models/storage_verification_request.rs
@@ -12,7 +12,7 @@ pub struct StorageVerificationRequest {
     pub contract_address: Vec<u8>,
     pub source_code: String,
     pub contract_name: String,
-    pub zk_compiler_version: String,
+    pub zk_compiler_version: Option<String>,
     pub compiler_version: String,
     pub optimization_used: bool,
     pub optimizer_mode: Option<String>,
diff --git a/core/lib/dal/src/storage_logs_dal.rs b/core/lib/dal/src/storage_logs_dal.rs
index adad6eb7e1db..4ed3e37a9a1d 100644
--- a/core/lib/dal/src/storage_logs_dal.rs
+++ b/core/lib/dal/src/storage_logs_dal.rs
@@ -225,60 +225,13 @@ impl StorageLogsDal<'_, '_> {
         Ok(())
     }
 
-    pub async fn is_contract_deployed_at_address(&mut self, address: Address) -> bool {
-        let hashed_key = get_code_key(&address).hashed_key();
-        let row = sqlx::query!(
-            r#"
-            SELECT
-                COUNT(*) AS "count!"
-            FROM
-                (
-                    SELECT
-                        *
-                    FROM
-                        storage_logs
-                    WHERE
-                        hashed_key = $1
-                        AND miniblock_number <= COALESCE(
-                            (
-                                SELECT
-                                    MAX(number)
-                                FROM
-                                    miniblocks
-                            ),
-                            (
-                                SELECT
-                                    miniblock_number
-                                FROM
-                                    snapshot_recovery
-                            )
-                        )
-                    ORDER BY
-                        miniblock_number DESC,
-                        operation_number DESC
-                    LIMIT
-                        1
-                ) sl
-            WHERE
-                sl.value != $2
-            "#,
-            hashed_key.as_bytes(),
-            FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes(),
-        )
-        .fetch_one(self.storage.conn())
-        .await
-        .unwrap();
-
-        row.count > 0
-    }
-
     /// Returns addresses and the corresponding deployment L2 block numbers among the specified contract
     /// `addresses`. `at_l2_block` allows filtering deployment by L2 blocks.
     pub async fn filter_deployed_contracts(
         &mut self,
         addresses: impl Iterator<Item = Address>,
         at_l2_block: Option<L2BlockNumber>,
-    ) -> DalResult<HashMap<Address, L2BlockNumber>> {
+    ) -> DalResult<HashMap<Address, (L2BlockNumber, H256)>> {
         let (bytecode_hashed_keys, address_by_hashed_key): (Vec<_>, HashMap<_, _>) = addresses
             .map(|address| {
                 let hashed_key = get_code_key(&address).hashed_key().0;
@@ -330,12 +283,13 @@ impl StorageLogsDal<'_, '_> {
         .await?;
 
         let deployment_data = rows.into_iter().filter_map(|row| {
-            if row.value == FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes() {
+            let bytecode_hash = H256::from_slice(&row.value);
+            if bytecode_hash == FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH {
                 return None;
             }
             let l2_block_number = L2BlockNumber(row.miniblock_number as u32);
             let address = address_by_hashed_key[row.hashed_key.as_slice()];
-            Some((address, l2_block_number))
+            Some((address, (l2_block_number, bytecode_hash)))
         });
         Ok(deployment_data.collect())
     }
@@ -1168,8 +1122,9 @@ mod tests {
     async fn filtering_deployed_contracts() {
         let contract_address = Address::repeat_byte(1);
         let other_contract_address = Address::repeat_byte(23);
+        let bytecode_hash = H256::repeat_byte(0xff);
         let successful_deployment =
-            StorageLog::new_write_log(get_code_key(&contract_address), H256::repeat_byte(0xff));
+            StorageLog::new_write_log(get_code_key(&contract_address), bytecode_hash);
         let failed_deployment = StorageLog::new_write_log(
             get_code_key(&contract_address),
             FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH,
@@ -1233,7 +1188,7 @@ mod tests {
                 .unwrap();
             assert_eq!(
                 deployed_map,
-                HashMap::from([(contract_address, L2BlockNumber(2))])
+                HashMap::from([(contract_address, (L2BlockNumber(2), bytecode_hash))])
             );
         }
 
@@ -1268,7 +1223,7 @@ mod tests {
             .unwrap();
         assert_eq!(
             deployed_map,
-            HashMap::from([(contract_address, L2BlockNumber(2))])
+            HashMap::from([(contract_address, (L2BlockNumber(2), bytecode_hash))])
         );
 
         for new_l2_block in [None, Some(L2BlockNumber(3))] {
@@ -1283,8 +1238,8 @@ mod tests {
             assert_eq!(
                 deployed_map,
                 HashMap::from([
-                    (contract_address, L2BlockNumber(2)),
-                    (other_contract_address, L2BlockNumber(3)),
+                    (contract_address, (L2BlockNumber(2), bytecode_hash)),
+                    (other_contract_address, (L2BlockNumber(3), bytecode_hash)),
                 ])
             );
         }
diff --git a/core/lib/dal/src/tokens_dal.rs b/core/lib/dal/src/tokens_dal.rs
index 218e152fa82a..b5fd67fc63c8 100644
--- a/core/lib/dal/src/tokens_dal.rs
+++ b/core/lib/dal/src/tokens_dal.rs
@@ -98,7 +98,7 @@ impl TokensDal<'_, '_> {
             .filter_map(|address| {
                 if address.is_zero() {
                     None
-                } else if let Some(deployed_at) = token_deployment_data.get(&address) {
+                } else if let Some((deployed_at, _)) = token_deployment_data.get(&address) {
                     (deployed_at > &block_number).then_some(address.0)
                 } else {
                     // Token belongs to a "pending" L2 block that's not yet fully inserted to the database.
diff --git a/core/lib/types/src/contract_verification_api.rs b/core/lib/types/src/contract_verification_api.rs
index fcaa1aa9a535..21e511549beb 100644
--- a/core/lib/types/src/contract_verification_api.rs
+++ b/core/lib/types/src/contract_verification_api.rs
@@ -152,17 +152,19 @@ pub enum CompilerType {
     Vyper,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(untagged)]
 pub enum CompilerVersions {
     #[serde(rename_all = "camelCase")]
     Solc {
-        compiler_zksolc_version: String, // FIXME: optional?
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        compiler_zksolc_version: Option<String>,
         compiler_solc_version: String,
     },
     #[serde(rename_all = "camelCase")]
     Vyper {
-        compiler_zkvyper_version: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        compiler_zkvyper_version: Option<String>,
         compiler_vyper_version: String,
     },
 }
@@ -175,16 +177,16 @@ impl CompilerVersions {
         }
     }
 
-    pub fn zk_compiler_version(&self) -> &str {
+    pub fn zk_compiler_version(&self) -> Option<&str> {
         match self {
             Self::Solc {
                 compiler_zksolc_version,
                 ..
-            } => compiler_zksolc_version,
+            } => compiler_zksolc_version.as_deref(),
             Self::Vyper {
                 compiler_zkvyper_version,
                 ..
-            } => compiler_zkvyper_version,
+            } => compiler_zkvyper_version.as_deref(),
         }
     }
 
diff --git a/core/node/contract_verification_server/Cargo.toml b/core/node/contract_verification_server/Cargo.toml
index eeb2c7828467..038347debc64 100644
--- a/core/node/contract_verification_server/Cargo.toml
+++ b/core/node/contract_verification_server/Cargo.toml
@@ -11,9 +11,9 @@ keywords.workspace = true
 categories.workspace = true
 
 [dependencies]
-zksync_config.workspace = true
 zksync_dal.workspace = true
 zksync_types.workspace = true
+zksync_utils.workspace = true
 vise.workspace = true
 
 anyhow.workspace = true
@@ -21,5 +21,11 @@ axum.workspace = true
 tokio = { workspace = true, features = ["time"] }
 tower-http = { workspace = true, features = ["cors"] }
 tracing.workspace = true
-serde.workspace = true
+
+[dev-dependencies]
+zksync_node_test_utils.workspace = true
+
+http-body-util.workspace = true
 serde_json.workspace = true
+test-casing.workspace = true
+tower.workspace = true
diff --git a/core/node/contract_verification_server/src/api_decl.rs b/core/node/contract_verification_server/src/api_decl.rs
index 256062936d32..d451cd79add9 100644
--- a/core/node/contract_verification_server/src/api_decl.rs
+++ b/core/node/contract_verification_server/src/api_decl.rs
@@ -3,10 +3,13 @@ use std::sync::Arc;
 use tower_http::cors::CorsLayer;
 use zksync_dal::{ConnectionPool, Core};
 
+use crate::cache::SupportedCompilersCache;
+
 #[derive(Debug, Clone)]
-pub struct RestApi {
-    pub(super) master_connection_pool: ConnectionPool<Core>,
-    pub(super) replica_connection_pool: ConnectionPool<Core>,
+pub(crate) struct RestApi {
+    pub(crate) master_connection_pool: ConnectionPool<Core>,
+    pub(crate) replica_connection_pool: ConnectionPool<Core>,
+    pub(crate) supported_compilers: Arc<SupportedCompilersCache>,
 }
 
 impl RestApi {
@@ -14,7 +17,9 @@ impl RestApi {
         master_connection_pool: ConnectionPool<Core>,
         replica_connection_pool: ConnectionPool<Core>,
     ) -> Self {
+        let supported_compilers = SupportedCompilersCache::new(replica_connection_pool.clone());
         Self {
+            supported_compilers: Arc::new(supported_compilers),
             master_connection_pool,
             replica_connection_pool,
         }
diff --git a/core/node/contract_verification_server/src/api_impl.rs b/core/node/contract_verification_server/src/api_impl.rs
index b8111e98a1cc..94be65673bad 100644
--- a/core/node/contract_verification_server/src/api_impl.rs
+++ b/core/node/contract_verification_server/src/api_impl.rs
@@ -1,195 +1,234 @@
-use std::sync::Arc;
+use std::{collections::HashSet, iter, sync::Arc};
 
+use anyhow::Context as _;
 use axum::{
     extract::{Path, State},
-    response::Response,
+    http::StatusCode,
+    response::{IntoResponse, Response},
     Json,
 };
-use serde::Serialize;
-use zksync_dal::CoreDal;
-use zksync_types::{contract_verification_api::VerificationIncomingRequest, Address};
+use zksync_dal::{CoreDal, DalError};
+use zksync_types::{
+    contract_verification_api::{
+        CompilerVersions, VerificationIncomingRequest, VerificationInfo, VerificationRequestStatus,
+    },
+    Address,
+};
+use zksync_utils::bytecode::BytecodeMarker;
 
 use super::{api_decl::RestApi, metrics::METRICS};
 
-fn ok_json(data: impl Serialize) -> Response<String> {
-    Response::builder()
-        .status(axum::http::StatusCode::OK)
-        .body(serde_json::to_string(&data).expect("Failed to serialize"))
-        .unwrap()
+#[derive(Debug)]
+pub(crate) enum ApiError {
+    IncorrectCompilerVersions,
+    UnsupportedCompilerVersions,
+    MissingZkCompilerVersion,
+    BogusZkCompilerVersion,
+    NoDeployedContract,
+    RequestNotFound,
+    VerificationInfoNotFound,
+    Internal(anyhow::Error),
+}
+
+impl From<anyhow::Error> for ApiError {
+    fn from(err: anyhow::Error) -> Self {
+        Self::Internal(err)
+    }
+}
+
+impl From<DalError> for ApiError {
+    fn from(err: DalError) -> Self {
+        Self::Internal(err.generalize())
+    }
 }
 
-fn bad_request(message: &str) -> Response<String> {
-    Response::builder()
-        .status(axum::http::StatusCode::BAD_REQUEST)
-        .body(message.to_string())
-        .unwrap()
+impl ApiError {
+    pub fn message(&self) -> &'static str {
+        match self {
+            Self::IncorrectCompilerVersions => "incorrect compiler versions",
+            Self::UnsupportedCompilerVersions => "unsupported compiler versions",
+            Self::MissingZkCompilerVersion => "missing zk compiler version for EraVM bytecode",
+            Self::BogusZkCompilerVersion => "zk compiler version specified for EVM bytecode",
+            Self::NoDeployedContract => "There is no deployed contract on this address",
+            Self::RequestNotFound => "request not found",
+            Self::VerificationInfoNotFound => "verification info not found for address",
+            Self::Internal(_) => "internal server error",
+        }
+    }
 }
 
-fn not_found() -> Response<String> {
-    Response::builder()
-        .status(axum::http::StatusCode::NOT_FOUND)
-        .body(String::new())
-        .unwrap()
+impl IntoResponse for ApiError {
+    fn into_response(self) -> Response {
+        let status_code = match &self {
+            Self::IncorrectCompilerVersions
+            | Self::UnsupportedCompilerVersions
+            | Self::MissingZkCompilerVersion
+            | Self::BogusZkCompilerVersion
+            | Self::NoDeployedContract => StatusCode::BAD_REQUEST,
+
+            Self::RequestNotFound | Self::VerificationInfoNotFound => StatusCode::NOT_FOUND,
+
+            Self::Internal(err) => {
+                // Do not expose the error details to the client, but log it.
+                tracing::warn!("Internal error: {err:#}");
+                StatusCode::INTERNAL_SERVER_ERROR
+            }
+        };
+        (status_code, self.message()).into_response()
+    }
 }
 
+type ApiResult<T> = Result<Json<T>, ApiError>;
+
 impl RestApi {
     #[tracing::instrument(skip(query))]
     fn validate_contract_verification_query(
         query: &VerificationIncomingRequest,
-    ) -> Result<(), Response<String>> {
+    ) -> Result<(), ApiError> {
         if query.source_code_data.compiler_type() != query.compiler_versions.compiler_type() {
-            return Err(bad_request("incorrect compiler versions"));
+            return Err(ApiError::IncorrectCompilerVersions);
         }
-
         Ok(())
     }
 
+    fn validate_compilers(
+        versions: &CompilerVersions,
+        bytecode_kind: BytecodeMarker,
+    ) -> Result<(), ApiError> {
+        match bytecode_kind {
+            BytecodeMarker::EraVm if versions.zk_compiler_version().is_none() => {
+                Err(ApiError::MissingZkCompilerVersion)
+            }
+            BytecodeMarker::Evm if versions.zk_compiler_version().is_some() => {
+                Err(ApiError::BogusZkCompilerVersion)
+            }
+            _ => Ok(()),
+        }
+    }
+
     /// Add a contract verification job to the queue if the requested contract wasn't previously verified.
+    // FIXME: this doesn't seem to check that the contract isn't verified; should it?
     #[tracing::instrument(skip(self_, request))]
     pub async fn verification(
         State(self_): State<Arc<Self>>,
         Json(request): Json<VerificationIncomingRequest>,
-    ) -> Response<String> {
+    ) -> ApiResult<usize> {
         let method_latency = METRICS.call[&"contract_verification"].start();
-        if let Err(res) = Self::validate_contract_verification_query(&request) {
-            return res;
+        Self::validate_contract_verification_query(&request)?;
+
+        let is_compilation_supported = self_
+            .supported_compilers
+            .get(|supported| supported.contain(&request.compiler_versions))
+            .await?;
+        if !is_compilation_supported {
+            return Err(ApiError::UnsupportedCompilerVersions);
         }
+
         let mut storage = self_
             .master_connection_pool
             .connection_tagged("api")
-            .await
-            .unwrap();
-
-        if !storage
+            .await?;
+        let deployment_info = storage
             .storage_logs_dal()
-            .is_contract_deployed_at_address(request.contract_address)
-            .await
-        {
-            return bad_request("There is no deployed contract on this address");
-        }
+            .filter_deployed_contracts(iter::once(request.contract_address), None)
+            .await?;
+        let &(_, bytecode_hash) = deployment_info
+            .get(&request.contract_address)
+            .ok_or(ApiError::NoDeployedContract)?;
+        let bytecode_marker = BytecodeMarker::new(bytecode_hash).with_context(|| {
+            format!(
+                "unknown bytecode marker for bytecode hash {bytecode_hash:?} at address {:?}",
+                request.contract_address
+            )
+        })?;
+        Self::validate_compilers(&request.compiler_versions, bytecode_marker)?;
 
         let request_id = storage
             .contract_verification_dal()
-            .add_contract_verification_request(request)
-            .await
-            .unwrap();
-
+            .add_contract_verification_request(&request)
+            .await?;
         method_latency.observe();
-        ok_json(request_id)
+        Ok(Json(request_id))
     }
 
     #[tracing::instrument(skip(self_))]
     pub async fn verification_request_status(
         State(self_): State<Arc<Self>>,
         id: Path<usize>,
-    ) -> Response<String> {
+    ) -> ApiResult<VerificationRequestStatus> {
         let method_latency = METRICS.call[&"contract_verification_request_status"].start();
         let status = self_
             .replica_connection_pool
             .connection_tagged("api")
-            .await
-            .unwrap()
+            .await?
             .contract_verification_dal()
             .get_verification_request_status(*id)
-            .await
-            .unwrap();
+            .await?
+            .ok_or(ApiError::RequestNotFound)?;
 
         method_latency.observe();
-        match status {
-            Some(status) => ok_json(status),
-            None => not_found(),
-        }
+        Ok(Json(status))
     }
 
     #[tracing::instrument(skip(self_))]
-    pub async fn zksolc_versions(State(self_): State<Arc<Self>>) -> Response<String> {
+    pub async fn zksolc_versions(State(self_): State<Arc<Self>>) -> ApiResult<HashSet<String>> {
         let method_latency = METRICS.call[&"contract_verification_zksolc_versions"].start();
         let versions = self_
-            .replica_connection_pool
-            .connection_tagged("api")
-            .await
-            .unwrap()
-            .contract_verification_dal()
-            .get_zksolc_versions()
-            .await
-            .unwrap();
-
+            .supported_compilers
+            .get(|supported| supported.zksolc.clone())
+            .await?;
         method_latency.observe();
-        ok_json(versions)
+        Ok(Json(versions))
     }
 
     #[tracing::instrument(skip(self_))]
-    pub async fn solc_versions(State(self_): State<Arc<Self>>) -> Response<String> {
+    pub async fn solc_versions(State(self_): State<Arc<Self>>) -> ApiResult<HashSet<String>> {
         let method_latency = METRICS.call[&"contract_verification_solc_versions"].start();
         let versions = self_
-            .replica_connection_pool
-            .connection_tagged("api")
-            .await
-            .unwrap()
-            .contract_verification_dal()
-            .get_solc_versions()
-            .await
-            .unwrap();
-
+            .supported_compilers
+            .get(|supported| supported.solc.clone())
+            .await?;
         method_latency.observe();
-        ok_json(versions)
+        Ok(Json(versions))
     }
 
     #[tracing::instrument(skip(self_))]
-    pub async fn zkvyper_versions(State(self_): State<Arc<Self>>) -> Response<String> {
+    pub async fn zkvyper_versions(State(self_): State<Arc<Self>>) -> ApiResult<HashSet<String>> {
         let method_latency = METRICS.call[&"contract_verification_zkvyper_versions"].start();
         let versions = self_
-            .replica_connection_pool
-            .connection_tagged("api")
-            .await
-            .unwrap()
-            .contract_verification_dal()
-            .get_zkvyper_versions()
-            .await
-            .unwrap();
-
+            .supported_compilers
+            .get(|supported| supported.zkvyper.clone())
+            .await?;
         method_latency.observe();
-        ok_json(versions)
+        Ok(Json(versions))
     }
 
     #[tracing::instrument(skip(self_))]
-    pub async fn vyper_versions(State(self_): State<Arc<Self>>) -> Response<String> {
+    pub async fn vyper_versions(State(self_): State<Arc<Self>>) -> ApiResult<HashSet<String>> {
         let method_latency = METRICS.call[&"contract_verification_vyper_versions"].start();
         let versions = self_
-            .replica_connection_pool
-            .connection_tagged("api")
-            .await
-            .unwrap()
-            .contract_verification_dal()
-            .get_vyper_versions()
-            .await
-            .unwrap();
-
+            .supported_compilers
+            .get(|supported| supported.vyper.clone())
+            .await?;
         method_latency.observe();
-        ok_json(versions)
+        Ok(Json(versions))
     }
 
     #[tracing::instrument(skip(self_))]
     pub async fn verification_info(
         State(self_): State<Arc<Self>>,
         address: Path<Address>,
-    ) -> Response<String> {
+    ) -> ApiResult<VerificationInfo> {
         let method_latency = METRICS.call[&"contract_verification_info"].start();
-
         let info = self_
             .replica_connection_pool
             .connection_tagged("api")
-            .await
-            .unwrap()
+            .await?
             .contract_verification_dal()
             .get_contract_verification_info(*address)
-            .await
-            .unwrap();
-
+            .await?
+            .ok_or(ApiError::VerificationInfoNotFound)?;
         method_latency.observe();
-        match info {
-            Some(info) => ok_json(info),
-            None => not_found(),
-        }
+        Ok(Json(info))
     }
 }
diff --git a/core/node/contract_verification_server/src/cache.rs b/core/node/contract_verification_server/src/cache.rs
new file mode 100644
index 000000000000..c8e367515287
--- /dev/null
+++ b/core/node/contract_verification_server/src/cache.rs
@@ -0,0 +1,122 @@
+use std::{
+    collections::HashSet,
+    time::{Duration, Instant},
+};
+
+use tokio::sync::RwLock;
+use zksync_dal::{Connection, ConnectionPool, Core, CoreDal, DalError};
+use zksync_types::contract_verification_api::CompilerVersions;
+
+/// Compiler versions supported by the contract verifier.
+#[derive(Debug, Clone)]
+pub(crate) struct SupportedCompilerVersions {
+    pub solc: HashSet<String>,
+    pub zksolc: HashSet<String>,
+    pub vyper: HashSet<String>,
+    pub zkvyper: HashSet<String>,
+}
+
+impl SupportedCompilerVersions {
+    /// Checks whether the supported compilers include ones specified in a request.
+    pub fn contain(&self, versions: &CompilerVersions) -> bool {
+        match versions {
+            CompilerVersions::Solc {
+                compiler_solc_version,
+                compiler_zksolc_version,
+            } => {
+                self.solc.contains(compiler_solc_version)
+                    && compiler_zksolc_version
+                        .as_ref()
+                        .map_or(true, |ver| self.zksolc.contains(ver))
+            }
+            CompilerVersions::Vyper {
+                compiler_vyper_version,
+                compiler_zkvyper_version,
+            } => {
+                self.vyper.contains(compiler_vyper_version)
+                    && compiler_zkvyper_version
+                        .as_ref()
+                        .map_or(true, |ver| self.zkvyper.contains(ver))
+            }
+        }
+    }
+}
+
+impl SupportedCompilerVersions {
+    async fn new(connection: &mut Connection<'_, Core>) -> Result<Self, DalError> {
+        let solc = connection
+            .contract_verification_dal()
+            .get_solc_versions()
+            .await?;
+        let zksolc = connection
+            .contract_verification_dal()
+            .get_zksolc_versions()
+            .await?;
+        let vyper = connection
+            .contract_verification_dal()
+            .get_vyper_versions()
+            .await?;
+        let zkvyper = connection
+            .contract_verification_dal()
+            .get_zkvyper_versions()
+            .await?;
+        Ok(Self {
+            solc: solc.into_iter().collect(),
+            zksolc: zksolc.into_iter().collect(),
+            vyper: vyper.into_iter().collect(),
+            zkvyper: zkvyper.into_iter().collect(),
+        })
+    }
+}
+
+/// Cache for compiler versions supported by the contract verifier.
+#[derive(Debug)]
+pub(crate) struct SupportedCompilersCache {
+    connection_pool: ConnectionPool<Core>,
+    inner: RwLock<Option<(SupportedCompilerVersions, Instant)>>,
+}
+
+impl SupportedCompilersCache {
+    const CACHE_UPDATE_INTERVAL: Duration = Duration::from_secs(10);
+
+    pub fn new(connection_pool: ConnectionPool<Core>) -> Self {
+        Self {
+            connection_pool,
+            inner: RwLock::new(None),
+        }
+    }
+
+    fn get_cached<R>(
+        cache: Option<&(SupportedCompilerVersions, Instant)>,
+        action: impl FnOnce(&SupportedCompilerVersions) -> R,
+    ) -> Option<R> {
+        cache.and_then(|(versions, updated_at)| {
+            (updated_at.elapsed() <= Self::CACHE_UPDATE_INTERVAL).then(|| action(versions))
+        })
+    }
+
+    pub async fn get<R>(
+        &self,
+        action: impl Fn(&SupportedCompilerVersions) -> R,
+    ) -> Result<R, DalError> {
+        let output = Self::get_cached(self.inner.read().await.as_ref(), &action);
+        if let Some(output) = output {
+            return Ok(output);
+        }
+
+        // We don't want to hold an exclusive lock while querying Postgres.
+        let supported = {
+            let mut connection = self.connection_pool.connection_tagged("api").await?;
+            let mut db_transaction = connection
+                .transaction_builder()?
+                .set_readonly()
+                .build()
+                .await?;
+            SupportedCompilerVersions::new(&mut db_transaction).await?
+        };
+        let output = action(&supported);
+        // Another task may have written to the cache already, but we should be fine with updating it again.
+        *self.inner.write().await = Some((supported, Instant::now()));
+        Ok(output)
+    }
+}
diff --git a/core/node/contract_verification_server/src/lib.rs b/core/node/contract_verification_server/src/lib.rs
index eea45f8564bf..912cec55f0b8 100644
--- a/core/node/contract_verification_server/src/lib.rs
+++ b/core/node/contract_verification_server/src/lib.rs
@@ -1,21 +1,24 @@
+use std::net::SocketAddr;
+
 use anyhow::Context as _;
 use tokio::sync::watch;
-use zksync_config::ContractVerifierConfig;
 use zksync_dal::ConnectionPool;
 
 use self::api_decl::RestApi;
 
 mod api_decl;
 mod api_impl;
+mod cache;
 mod metrics;
+#[cfg(test)]
+mod tests;
 
 pub async fn start_server(
     master_connection_pool: ConnectionPool<zksync_dal::Core>,
     replica_connection_pool: ConnectionPool<zksync_dal::Core>,
-    config: ContractVerifierConfig,
+    bind_address: SocketAddr,
     mut stop_receiver: watch::Receiver<bool>,
 ) -> anyhow::Result<()> {
-    let bind_address = config.bind_addr();
     let api = RestApi::new(master_connection_pool, replica_connection_pool).into_router();
 
     let listener = tokio::net::TcpListener::bind(bind_address)
diff --git a/core/node/contract_verification_server/src/tests.rs b/core/node/contract_verification_server/src/tests.rs
new file mode 100644
index 000000000000..b7b0d3e8efb4
--- /dev/null
+++ b/core/node/contract_verification_server/src/tests.rs
@@ -0,0 +1,356 @@
+//! Tests for contract verification API server.
+
+use std::{str, time::Duration};
+
+use axum::{
+    body::Body,
+    http::{header, Method, Request, Response, StatusCode},
+};
+use http_body_util::BodyExt as _;
+use test_casing::test_casing;
+use tower::ServiceExt;
+use zksync_dal::{Connection, Core, CoreDal};
+use zksync_node_test_utils::create_l2_block;
+use zksync_types::{
+    contract_verification_api::CompilerVersions, get_code_key, Address, L2BlockNumber,
+    ProtocolVersion, StorageLog,
+};
+use zksync_utils::bytecode::{hash_bytecode, hash_evm_bytecode, BytecodeMarker};
+
+use super::*;
+use crate::api_impl::ApiError;
+
+const SOLC_VERSION: &str = "0.8.27";
+const ZKSOLC_VERSION: &str = "1.5.6";
+
+async fn prepare_storage(storage: &mut Connection<'_, Core>) {
+    storage
+        .protocol_versions_dal()
+        .save_protocol_version_with_tx(&ProtocolVersion::default())
+        .await
+        .unwrap();
+    storage
+        .blocks_dal()
+        .insert_l2_block(&create_l2_block(0))
+        .await
+        .unwrap();
+
+    storage
+        .contract_verification_dal()
+        .set_solc_versions(&[SOLC_VERSION.to_owned()])
+        .await
+        .unwrap();
+    storage
+        .contract_verification_dal()
+        .set_zksolc_versions(&[ZKSOLC_VERSION.to_owned()])
+        .await
+        .unwrap();
+}
+
+async fn mock_deploy_contract(
+    storage: &mut Connection<'_, Core>,
+    address: Address,
+    kind: BytecodeMarker,
+) {
+    let bytecode_hash = match kind {
+        BytecodeMarker::EraVm => hash_bytecode(&[0; 32]),
+        BytecodeMarker::Evm => hash_evm_bytecode(&[0; 96]),
+    };
+    let deploy_log = StorageLog::new_write_log(get_code_key(&address), bytecode_hash);
+    storage
+        .storage_logs_dal()
+        .append_storage_logs(L2BlockNumber(0), &[deploy_log])
+        .await
+        .unwrap()
+}
+
+fn post_request(body: &serde_json::Value) -> Request<Body> {
+    Request::builder()
+        .method(Method::POST)
+        .uri("/contract_verification")
+        .header(header::CONTENT_TYPE, "application/json")
+        .body(Body::from(serde_json::to_vec(body).unwrap()))
+        .unwrap()
+}
+
+async fn json_response(response: Response<Body>) -> serde_json::Value {
+    assert_eq!(response.status(), StatusCode::OK);
+    assert_eq!(
+        response.headers().get(header::CONTENT_TYPE).unwrap(),
+        "application/json"
+    );
+    let response = response.into_body();
+    let response = response.collect().await.unwrap().to_bytes();
+    serde_json::from_slice(&response).unwrap()
+}
+
+#[tokio::test]
+async fn getting_compiler_versions() {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let req = Request::builder()
+        .method(Method::GET)
+        .uri("/contract_verification/zksolc_versions")
+        .body(Body::empty())
+        .unwrap();
+    let response = router.clone().oneshot(req).await.unwrap();
+    let versions = json_response(response).await;
+    assert_eq!(versions, serde_json::json!([ZKSOLC_VERSION]));
+
+    let req = Request::builder()
+        .method(Method::GET)
+        .uri("/contract_verification/solc_versions")
+        .body(Body::empty())
+        .unwrap();
+    let response = router.oneshot(req).await.unwrap();
+    let versions = json_response(response).await;
+    assert_eq!(versions, serde_json::json!([SOLC_VERSION]));
+}
+
+#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])]
+#[tokio::test]
+async fn submitting_request(bytecode_kind: BytecodeMarker) {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+
+    let address = Address::repeat_byte(0x23);
+    let verification_request = serde_json::json!({
+        "contractAddress": address,
+        "sourceCode": "contract Test {}",
+        "contractName": "Test",
+        "compilerZksolcVersion": match bytecode_kind {
+            BytecodeMarker::EraVm => Some(ZKSOLC_VERSION),
+            BytecodeMarker::Evm => None,
+        },
+        "compilerSolcVersion": SOLC_VERSION,
+        "optimizationUsed": true,
+    });
+
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let response = router
+        .clone()
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+    assert_eq!(response.status(), StatusCode::BAD_REQUEST); // the address is not deployed to
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(error_message, ApiError::NoDeployedContract.message());
+
+    mock_deploy_contract(&mut storage, address, bytecode_kind).await;
+
+    let response = router
+        .clone()
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+    let id = json_response(response).await;
+    assert_eq!(id, serde_json::json!(1));
+
+    let request = storage
+        .contract_verification_dal()
+        .get_next_queued_verification_request(Duration::from_secs(600))
+        .await
+        .unwrap()
+        .expect("request not persisted");
+    assert_eq!(request.id, 1);
+    assert_eq!(request.req.contract_address, address);
+    assert_eq!(
+        request.req.compiler_versions,
+        CompilerVersions::Solc {
+            compiler_zksolc_version: match bytecode_kind {
+                BytecodeMarker::EraVm => Some(ZKSOLC_VERSION.to_owned()),
+                BytecodeMarker::Evm => None,
+            },
+            compiler_solc_version: SOLC_VERSION.to_owned(),
+        }
+    );
+    assert_eq!(request.req.contract_name, "Test");
+    assert!(request.req.optimization_used);
+
+    let req = Request::builder()
+        .method(Method::GET)
+        .uri("/contract_verification/1")
+        .body(Body::empty())
+        .unwrap();
+    let response = router.oneshot(req).await.unwrap();
+    let request_status = json_response(response).await;
+    assert_eq!(request_status["status"], "in_progress");
+}
+
+#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])]
+#[tokio::test]
+async fn submitting_request_with_invalid_compiler_type(bytecode_kind: BytecodeMarker) {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+
+    let address = Address::repeat_byte(0x23);
+    mock_deploy_contract(&mut storage, address, bytecode_kind).await;
+
+    let verification_request = serde_json::json!({
+        "contractAddress": address,
+        "sourceCode": "contract Test {}",
+        "contractName": "Test",
+        // Intentionally incorrect versions "shape"
+        "compilerZksolcVersion": match bytecode_kind {
+            BytecodeMarker::Evm => Some(ZKSOLC_VERSION),
+            BytecodeMarker::EraVm => None,
+        },
+        "compilerSolcVersion": SOLC_VERSION,
+        "optimizationUsed": true,
+    });
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let response = router
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+
+    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    let expected_message = match bytecode_kind {
+        BytecodeMarker::Evm => ApiError::BogusZkCompilerVersion.message(),
+        BytecodeMarker::EraVm => ApiError::MissingZkCompilerVersion.message(),
+    };
+    assert_eq!(error_message, expected_message);
+}
+
+#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])]
+#[tokio::test]
+async fn submitting_request_with_unsupported_solc(bytecode_kind: BytecodeMarker) {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+
+    let address = Address::repeat_byte(0x23);
+    mock_deploy_contract(&mut storage, address, bytecode_kind).await;
+
+    let verification_request = serde_json::json!({
+        "contractAddress": address,
+        "sourceCode": "contract Test {}",
+        "contractName": "Test",
+        "compilerZksolcVersion": match bytecode_kind {
+            BytecodeMarker::Evm => None,
+            BytecodeMarker::EraVm => Some(ZKSOLC_VERSION),
+        },
+        "compilerSolcVersion": "1.0.0",
+        "optimizationUsed": true,
+    });
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let response = router
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+
+    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(
+        error_message,
+        ApiError::UnsupportedCompilerVersions.message()
+    );
+}
+
+#[tokio::test]
+async fn submitting_request_with_unsupported_zksolc() {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+
+    let address = Address::repeat_byte(0x23);
+    mock_deploy_contract(&mut storage, address, BytecodeMarker::EraVm).await;
+
+    let verification_request = serde_json::json!({
+        "contractAddress": address,
+        "sourceCode": "contract Test {}",
+        "contractName": "Test",
+        "compilerZksolcVersion": "1000.0.0",
+        "compilerSolcVersion": SOLC_VERSION,
+        "optimizationUsed": true,
+    });
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let response = router
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+
+    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(
+        error_message,
+        ApiError::UnsupportedCompilerVersions.message()
+    );
+}
+
+#[tokio::test]
+async fn querying_missing_request() {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+    let router = RestApi::new(pool.clone(), pool).into_router();
+
+    let req = Request::builder()
+        .method(Method::GET)
+        .uri("/contract_verification/1")
+        .body(Body::empty())
+        .unwrap();
+    let response = router.oneshot(req).await.unwrap();
+
+    assert_eq!(response.status(), StatusCode::NOT_FOUND);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(error_message, ApiError::RequestNotFound.message());
+}
+
+#[tokio::test]
+async fn querying_missing_verification_info() {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+    let router = RestApi::new(pool.clone(), pool).into_router();
+
+    let req = Request::builder()
+        .method(Method::GET)
+        .uri("/contract_verification/info/0x2323232323232323232323232323232323232323")
+        .body(Body::empty())
+        .unwrap();
+    let response = router.oneshot(req).await.unwrap();
+
+    assert_eq!(response.status(), StatusCode::NOT_FOUND);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(error_message, ApiError::VerificationInfoNotFound.message());
+}
+
+#[tokio::test]
+async fn mismatched_compiler_type() {
+    let pool = ConnectionPool::test_pool().await;
+    let mut storage = pool.connection().await.unwrap();
+    prepare_storage(&mut storage).await;
+    let address = Address::repeat_byte(0x23);
+    mock_deploy_contract(&mut storage, address, BytecodeMarker::EraVm).await;
+
+    let verification_request = serde_json::json!({
+        "contractAddress": address,
+        "sourceCode": "contract Test {}",
+        "contractName": "Test",
+        "compilerVyperVersion": "1.0.1",
+        "optimizationUsed": true,
+    });
+
+    let router = RestApi::new(pool.clone(), pool).into_router();
+    let response = router
+        .oneshot(post_request(&verification_request))
+        .await
+        .unwrap();
+    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+    let error_message = response.collect().await.unwrap().to_bytes();
+    let error_message = str::from_utf8(&error_message).unwrap();
+    assert_eq!(error_message, ApiError::IncorrectCompilerVersions.message());
+}
diff --git a/core/node/node_framework/src/implementations/layers/contract_verification_api.rs b/core/node/node_framework/src/implementations/layers/contract_verification_api.rs
index 3f1f76cc1c12..2ca7cc25a1fd 100644
--- a/core/node/node_framework/src/implementations/layers/contract_verification_api.rs
+++ b/core/node/node_framework/src/implementations/layers/contract_verification_api.rs
@@ -69,7 +69,7 @@ impl Task for ContractVerificationApiTask {
         zksync_contract_verification_server::start_server(
             self.master_pool,
             self.replica_pool,
-            self.config,
+            self.config.bind_addr(),
             stop_receiver.0,
         )
         .await
diff --git a/zkstack_cli/Cargo.lock b/zkstack_cli/Cargo.lock
index 0f0657c60278..a9089719714d 100644
--- a/zkstack_cli/Cargo.lock
+++ b/zkstack_cli/Cargo.lock
@@ -1097,7 +1097,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
- "serde",
 ]
 
 [[package]]
@@ -6808,11 +6807,6 @@ dependencies = [
  "rand",
  "secrecy",
  "serde",
- "strum",
- "strum_macros",
- "time",
- "url",
- "vise",
  "zksync_basic_types",
  "zksync_concurrency",
  "zksync_consensus_utils",
@@ -6962,7 +6956,6 @@ dependencies = [
  "secrecy",
  "serde_json",
  "serde_yaml",
- "time",
  "tracing",
  "zksync_basic_types",
  "zksync_config",