diff --git a/gradle.properties b/gradle.properties
index 6d9818e4f2b94e490c04dd43e3cd059d76f6a19b..c0b442fc71633eafaacc06c4744bdc007862b55a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ micronautRxVersion = 2.1.1
 micronautPluginVersion = 3.1.1
 nodePluginVersion = 3.0.1
 nodeVersion = 16.3.0
-nshmpLibVersion = 0.9.7
+nshmpLibVersion = 0.9.9
 nshmpWsUtilsVersion = 0.1.7
 shadowVersion = 7.1.2
 spotbugsVersion = 4.7.0
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
index 2b268e5b92ef29a307470c56d5df8b6680c8c077..569ec1a3d09bbb67e77a7268efcfbf2ceb6a5a1b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -155,8 +156,16 @@ public class DisaggCalc {
       int colsToSkip = siteColumns.size(); // needed?
       log.info("Site data columns: " + colsToSkip);
 
+      /* Possible batch vs30 from config.vs30s. */
+      checkArgument(
+          config.hazard.vs30s.size() <= 1,
+          "config.hazard.vs30s may only have one value for disagg");
+      OptionalDouble vs30 = (config.hazard.vs30s.size() == 1)
+          ? OptionalDouble.of(config.hazard.vs30s.iterator().next())
+          : OptionalDouble.empty();
+
       /* Sites */
-      List<Site> sites = Sites.fromCsv(siteFile, config, model.siteData());
+      List<Site> sites = Sites.fromCsv(siteFile, model.siteData(), vs30);
       log.info("Sites: " + sites.size());
 
       Set<Imt> modelImts = model.config().hazard.imts;
@@ -280,7 +289,7 @@ public class DisaggCalc {
 
     log.info(PROGRAM + " (return period): calculating ...");
 
-    HazardExport handler = HazardExport.create(model, config, sites);
+    HazardExport handler = HazardExport.create(model, config, sites, OptionalDouble.empty());
     Path disaggDir = handler.outputDir().resolve("disagg");
     Files.createDirectory(disaggDir);
 
@@ -369,6 +378,7 @@ public class DisaggCalc {
     }
 
     log.info(PROGRAM + " (IML): calculating ...");
+
     Path outDir = createOutputDir(config.output.directory);
     Path disaggDir = outDir.resolve("disagg");
     Files.createDirectory(disaggDir);
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
index 3342bca22cd0ef921c542fb752dd5a1907e1423d..4040521bef2279741808d36e231c73055b212d7e 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
@@ -11,6 +11,7 @@ import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
@@ -112,10 +113,25 @@ public class HazardCalc {
       log.info(config.toString());
 
       log.info("");
-      List<Site> sites = readSites(args[1], config, model.siteData(), log);
-      log.info("Sites: " + Sites.toString(sites));
 
-      Path out = calc(model, config, sites, log);
+      Path out = null;
+      if (config.hazard.vs30s.isEmpty()) {
+
+        List<Site> sites = readSites(args[1], model.siteData(), OptionalDouble.empty(), log);
+        log.info("Sites: " + Sites.toString(sites));
+        out = calc(model, config, sites, OptionalDouble.empty(), log);
+
+      } else {
+
+        for (double vs30 : config.hazard.vs30s) {
+          log.info("Vs30 batch: " + vs30);
+          List<Site> sites = readSites(args[1], model.siteData(), OptionalDouble.of(vs30), log);
+          log.info("Sites: " + Sites.toString(sites));
+          out = calc(model, config, sites, OptionalDouble.of(vs30), log);
+        }
+        out = checkNotNull(out.getParent());
+
+      }
 
       if (config.output.dataTypes.contains(DataType.MAP)) {
         HazardMaps.createDataSets(out, config.output.returnPeriods, log);
@@ -137,8 +153,8 @@ public class HazardCalc {
 
   static List<Site> readSites(
       String arg,
-      CalcConfig defaults,
       SiteData siteData,
+      OptionalDouble vs30,
       Logger log) {
 
     Path path = Paths.get(arg);
@@ -149,8 +165,8 @@ public class HazardCalc {
 
     try {
       return fname.endsWith(".csv")
-          ? Sites.fromCsv(path, defaults, siteData)
-          : Sites.fromGeoJson(path, defaults, siteData);
+          ? Sites.fromCsv(path, siteData, vs30)
+          : Sites.fromGeoJson(path, siteData, vs30);
     } catch (IOException ioe) {
       throw new IllegalArgumentException(
           "Error parsing sites file [%s]; see sites file documentation");
@@ -165,6 +181,7 @@ public class HazardCalc {
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
+      OptionalDouble vs30,
       Logger log) throws IOException, InterruptedException, ExecutionException {
 
     int threadCount = config.performance.threadCount.value();
@@ -172,7 +189,7 @@ public class HazardCalc {
     log.info("Threads: " + ((ThreadPoolExecutor) exec).getCorePoolSize());
     log.info(PROGRAM + ": calculating ...");
 
-    HazardExport handler = HazardExport.create(model, config, sites);
+    HazardExport handler = HazardExport.create(model, config, sites, vs30);
     CalcTask.Builder calcTask = new CalcTask.Builder(model, config, exec);
     WriteTask.Builder writeTask = new WriteTask.Builder(handler);
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
index 581b40dca6728473e1965fdd02f1bc06070f5b96..d0febbdef75ca9402b01c2822fa6e4d7b3c3d97d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
@@ -10,6 +10,7 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -107,7 +108,8 @@ public class RateCalc {
       log.info(config.toString());
 
       log.info("");
-      List<Site> sites = HazardCalc.readSites(args[1], config, model.siteData(), log);
+      List<Site> sites = HazardCalc.readSites(
+          args[1], model.siteData(), OptionalDouble.empty(), log);
       log.info("Sites: " + Sites.toString(sites));
 
       Path out = calc(model, config, sites, log);
diff --git a/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java b/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
index a6de60423ee7ab90bd07b04cb46a9441581f24c5..b18913f725d0367f5b9354842db1b0730cec99b1 100644
--- a/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
+++ b/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
@@ -16,6 +16,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalDouble;
 import java.util.concurrent.ExecutorService;
 import java.util.logging.LogManager;
 import java.util.stream.Collectors;
@@ -138,10 +139,10 @@ class PeerTest {
     Map<String, double[]> expectedsMap = loadExpecteds(modelId);
     HazardModel model = HazardModel.load(MODEL_DIR.resolve(modelId));
     CalcConfig config = model.config();
-    Iterable<Site> sites = Sites.fromCsv(MODEL_DIR.resolve(
-        modelId).resolve("sites.csv"),
-        config,
-        model.siteData());
+    Iterable<Site> sites = Sites.fromCsv(
+        MODEL_DIR.resolve(modelId).resolve("sites.csv"),
+        model.siteData(),
+        OptionalDouble.empty());
 
     // ensure that only PGA is being used
     checkState(config.hazard.imts.size() == 1);