diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ServicesUtil.java
similarity index 88%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/ServicesUtil.java
index b2da3f0917178c67b8acb98f9da86f899e1ec652..0da1909789abb10235cbf9f1116ee2b088a819fb 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ServicesUtil.java
@@ -1,4 +1,4 @@
-package gov.usgs.earthquake.nshmp.www.services;
+package gov.usgs.earthquake.nshmp.www;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
@@ -11,9 +11,6 @@ import gov.usgs.earthquake.nshmp.calc.Hazard;
 import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.model.HazardModel;
-import gov.usgs.earthquake.nshmp.www.ResponseBody;
-import gov.usgs.earthquake.nshmp.www.WsUtils;
-
 import io.micronaut.http.HttpResponse;
 
 public class ServicesUtil {
@@ -35,7 +32,8 @@ public class ServicesUtil {
     return HttpResponse.serverError(response);
   }
 
-  static Hazard calcHazard(
+  @Deprecated
+  public static Hazard calcHazard(
       Function<HazardModel, CalcConfig> configFunction,
       Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException {
 
@@ -47,12 +45,12 @@ public class ServicesUtil {
   }
 
   @Deprecated
-  static class ServiceQueryData implements ServiceQuery {
+  public static class ServiceQueryData implements ServiceQuery {
 
     public final Double longitude;
     public final Double latitude;
 
-    ServiceQueryData(Double longitude, Double latitude) {
+    public ServiceQueryData(Double longitude, Double latitude) {
       this.longitude = longitude;
       this.latitude = latitude;
     }
@@ -70,7 +68,7 @@ public class ServicesUtil {
   }
 
   @Deprecated
-  static class ServiceRequestData {
+  public static class ServiceRequestData {
 
     public final double longitude;
     public final double latitude;
@@ -81,7 +79,7 @@ public class ServicesUtil {
     }
   }
 
-  enum Key {
+  public enum Key {
     EDITION,
     REGION,
     MODEL,
@@ -114,6 +112,7 @@ public class ServicesUtil {
     void checkValues();
   }
 
+  @Deprecated
   private static CompletableFuture<Hazard> calcHazard(
       HazardModel model,
       CalcConfig config,
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java
similarity index 94%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java
index 81651afd7d44ac3d968008892b7e7d1dc03700eb..3de64116aa99b3bfacebdbc762657fc9bccd3a13 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java
@@ -1,4 +1,4 @@
-package gov.usgs.earthquake.nshmp.www.services;
+package gov.usgs.earthquake.nshmp.www;
 
 import static java.lang.Runtime.getRuntime;
 
@@ -27,9 +27,7 @@ import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.ValueFormat;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.model.HazardModel;
-import gov.usgs.earthquake.nshmp.www.WsUtils;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
-
 import io.micronaut.context.annotation.Value;
 import io.micronaut.context.event.ShutdownEvent;
 import io.micronaut.context.event.StartupEvent;
@@ -46,10 +44,10 @@ public class ServletUtil {
 
   public static final Gson GSON;
 
-  static final ListeningExecutorService CALC_EXECUTOR;
-  static final ExecutorService TASK_EXECUTOR;
+  public static final ListeningExecutorService CALC_EXECUTOR;
+  public static final ExecutorService TASK_EXECUTOR;
 
-  static final int THREAD_COUNT;
+  public static final int THREAD_COUNT;
 
   @Value("${nshmp-haz.model-path}")
   private Path modelPath;
@@ -73,7 +71,7 @@ public class ServletUtil {
         .create();
   }
 
-  static HazardModel model() {
+  public static HazardModel model() {
     return HAZARD_MODEL;
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java
index c2251caba5d3ed3612ed43e1aa8ca14404f7f381..6cd6d6d04a827c18eae70c9852878e5f5adf833d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java
@@ -5,8 +5,6 @@ import java.util.stream.Collectors;
 
 import com.google.common.io.Resources;
 
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil;
-
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
 import io.micronaut.http.MediaType;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazardController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardController.java
similarity index 63%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/HazardController.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardController.java
index fdc74747815374a10fb5977cf022ceb16a05319a..54c765def174142b7456525caa90fb62160be1fe 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazardController.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardController.java
@@ -1,8 +1,7 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import gov.usgs.earthquake.nshmp.www.services.HazardService;
-import gov.usgs.earthquake.nshmp.www.services.HazardService.QueryParameters;
+package gov.usgs.earthquake.nshmp.www.hazard;
 
+import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil;
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
 import io.micronaut.http.annotation.Controller;
@@ -16,9 +15,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.inject.Inject;
 
 /**
- * Micronaut controller for probabilistic seismic hazard calculations.
+ * Micronaut controller for probabilistic seismic hazard calculations and
+ * services.
  *
- * @see HazardService
  * @author U.S. Geological Survey
  */
 @Tag(
@@ -32,8 +31,7 @@ public class HazardController {
 
   @Operation(
       summary = "Hazard model and service metadata",
-      description = "Returns details of the installed model and service request parameters",
-      operationId = "hazard_doGetMetadata")
+      description = "Returns details of the installed model and service request parameters")
   @ApiResponse(
       description = "Hazard service metadata",
       responseCode = "200")
@@ -51,31 +49,32 @@ public class HazardController {
    */
   @Operation(
       summary = "Compute probabilisitic hazard at a site",
-      description = "Returns hazard curves computed from the installed model",
-      operationId = "hazard_doGetHazard")
+      description = "Returns hazard curves computed from the installed model")
   @ApiResponse(
       description = "Hazard curves",
       responseCode = "200")
   @Get(uri = "/{longitude}/{latitude}/{vs30}{?truncate,maxdir}")
   public HttpResponse<String> doGetHazard(
-      HttpRequest<?> request,
-
-      @Schema(minimum = "-360", maximum = "360") @PathVariable double longitude,
-
-      @Schema(minimum = "-90", maximum = "90") @PathVariable double latitude,
-
-      @Schema(minimum = "150", maximum = "3000") @PathVariable int vs30,
-
-      @QueryValue(defaultValue = "false") boolean truncate,
-
-      @QueryValue(defaultValue = "false") boolean maxdir) {
-
-    /*
-     * @Schema annotation parameter constraints only affect Swagger service
-     * index page behavior; still need to validate against model. TODO
-     */
-
-    var query = new QueryParameters(longitude, latitude, vs30, truncate, maxdir);
-    return HazardService.handleDoGetHazard(request, query);
+      HttpRequest<?> http,
+      @Schema(
+          minimum = "-360",
+          maximum = "360") @PathVariable double longitude,
+      @Schema(
+          minimum = "-90",
+          maximum = "90") @PathVariable double latitude,
+      @Schema(
+          minimum = "150",
+          maximum = "3000") @PathVariable int vs30,
+      @QueryValue(
+          defaultValue = "false") boolean truncate,
+      @QueryValue(
+          defaultValue = "false") boolean maxdir) {
+    try {
+      HazardService.Request request = new HazardService.Request(
+          http, longitude, latitude, vs30, truncate, maxdir);
+      return HazardService.processRequest(request);
+    } catch (Exception e) {
+      return ServicesUtil.handleError(e, HazardService.NAME, http.getUri().getPath());
+    }
   }
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardService.java
similarity index 62%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardService.java
index 89f03a87b3a4d52e36b209be6f79799729b46a51..90cf98ef5a97a0d7b67f388695d06484d438c09f 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/HazardService.java
@@ -1,20 +1,24 @@
-package gov.usgs.earthquake.nshmp.www.services;
+package gov.usgs.earthquake.nshmp.www.hazard;
 
 import static com.google.common.base.Preconditions.checkState;
 import static gov.usgs.earthquake.nshmp.calc.HazardExport.curvesBySource;
+import static gov.usgs.earthquake.nshmp.data.DoubleData.checkInRange;
+import static gov.usgs.earthquake.nshmp.geo.Coordinates.checkLatitude;
+import static gov.usgs.earthquake.nshmp.geo.Coordinates.checkLongitude;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
 
 import com.google.common.base.Stopwatch;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Hazard;
+import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.data.MutableXySequence;
 import gov.usgs.earthquake.nshmp.data.XySequence;
@@ -23,16 +27,13 @@ import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.model.HazardModel;
 import gov.usgs.earthquake.nshmp.model.SourceType;
-import gov.usgs.earthquake.nshmp.www.HazardController;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
-import gov.usgs.earthquake.nshmp.www.WsUtils;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil;
+import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.meta.Parameter;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
-
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
 import jakarta.inject.Singleton;
@@ -46,13 +47,13 @@ import jakarta.inject.Singleton;
 @Singleton
 public final class HazardService {
 
-  private static final String NAME = "Hazard Service";
+  static final String NAME = "Hazard Service";
 
   /** HazardController.doGetUsage() handler. */
   public static HttpResponse<String> handleDoGetMetadata(HttpRequest<?> request) {
     var url = request.getUri().getPath();
     try {
-      var usage = new RequestMetadata(ServletUtil.model());
+      var usage = new UsageMetadata(ServletUtil.model());
       var response = ResponseBody.usage()
           .name(NAME)
           .url(url)
@@ -67,111 +68,82 @@ public final class HazardService {
   }
 
   /** HazardController.doGetHazard() handler. */
-  public static HttpResponse<String> handleDoGetHazard(
-      HttpRequest<?> request,
-      QueryParameters query) {
-
+  public static HttpResponse<String> processRequest(Request request) {
     try {
-      // TODO still need to validate
-      // if (query.isEmpty()) {
-      // return handleDoGetUsage(urlHelper);
-      // }
-      // query.checkParameters();
-      var data = new RequestData(query);
-      var response = process(request, data);
-      var svcResponse = ServletUtil.GSON.toJson(response);
+      Response response = process(request);
+      var body = ResponseBody.success()
+          .name(NAME)
+          .url(request.http.getUri().getPath())
+          .request(request)
+          .response(response)
+          .build();
+      String svcResponse = ServletUtil.GSON.toJson(body);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, request.getUri().getPath());
+      return ServicesUtil.handleError(e, NAME, request.http.getUri().getPath());
     }
   }
 
-  static ResponseBody<RequestData, ResponseData> process(
-      HttpRequest<?> request,
-      RequestData data)
+  /*
+   * Developer notes:
+   *
+   * Future calculation configuration options: vertical GMs
+   *
+   * NSHM Hazard Tool will not pass truncation and maxdir args/flags as the apps
+   * apply truncation and scaling on the client.
+   */
+
+  static Response process(Request request)
       throws InterruptedException, ExecutionException {
 
-    var configFunction = new ConfigFunction();
-    var siteFunction = new SiteFunction(data);
     var stopwatch = Stopwatch.createStarted();
-    var hazard = ServicesUtil.calcHazard(configFunction, siteFunction);
+    var hazard = calcHazard(request);
 
     return new ResultBuilder()
+        .request(request)
         .hazard(hazard)
-        .requestData(data)
         .timer(stopwatch)
-        .url(request)
         .build();
   }
 
-  static class ConfigFunction implements Function<HazardModel, CalcConfig> {
-    @Override
-    public CalcConfig apply(HazardModel model) {
-      var configBuilder = CalcConfig.copyOf(model.config());
-      return configBuilder.build();
-    }
-  }
+  public static Hazard calcHazard(Request request)
+      throws InterruptedException, ExecutionException {
 
-  static class SiteFunction implements Function<CalcConfig, Site> {
-    final RequestData data;
+    HazardModel model = ServletUtil.model();
 
-    private SiteFunction(RequestData data) {
-      this.data = data;
-    }
+    // will we be passing in options for config??
+    CalcConfig config = CalcConfig.copyOf(model.config()).build();
 
-    @Override // TODO this needs to pick up SiteData
-    public Site apply(CalcConfig config) {
-      return Site.builder()
-          .location(Location.create(data.longitude, data.latitude))
-          .vs30(data.vs30)
-          .build();
-    }
+    // TODO this needs to pick up SiteData
+    Site site = Site.builder()
+        .location(Location.create(request.longitude, request.latitude))
+        .vs30(request.vs30)
+        .build();
+    CompletableFuture<Hazard> future = futureHazard(model, config, site);
+    return future.get();
   }
 
-  public static class QueryParameters {
+  private static CompletableFuture<Hazard> futureHazard(
+      HazardModel model,
+      CalcConfig config,
+      Site site) {
 
-    final double longitude;
-    final double latitude;
-    final int vs30;
-    final boolean truncate;
-    final boolean maxdir;
-
-    public QueryParameters(
-        double longitude,
-        double latitude,
-        int vs30,
-        boolean truncate,
-        boolean maxdir) {
-
-      this.longitude = longitude;
-      this.latitude = latitude;
-      this.vs30 = vs30;
-      this.truncate = truncate;
-      this.maxdir = maxdir;
-    }
-
-    // void checkParameters() {
-    // checkParameter(longitude, "longitude");
-    // checkParameter(latitude, "latitude");
-    // checkParameter(vs30, "vs30");
-    // }
+    return CompletableFuture.supplyAsync(
+        () -> HazardCalcs.hazard(model, config, site, ServletUtil.CALC_EXECUTOR),
+        ServletUtil.TASK_EXECUTOR);
   }
 
-  // private static void checkParameter(Object param, String id) {
-  // checkNotNull(param, "Missing parameter: %s", id);
-  // // TODO check range here
-  // }
-
-  /* Service request and model metadata */
-  static class RequestMetadata {
+  private static class UsageMetadata {
 
     final SourceModel model;
     final DoubleParameter longitude;
     final DoubleParameter latitude;
     final DoubleParameter vs30;
 
-    RequestMetadata(HazardModel model) {
+    UsageMetadata(HazardModel model) {
       this.model = new SourceModel(model);
+      // perhaps move out to shared factory with parameter instances
+      //
       // TODO need min max from model
       longitude = new DoubleParameter(
           "Longitude",
@@ -186,27 +158,36 @@ public final class HazardService {
           Coordinates.LAT_RANGE.upperEndpoint());
 
       vs30 = new DoubleParameter(
-          "Latitude",
+          "Vs30",
           "m/s",
           150,
           1500);
     }
   }
 
-  static class RequestData {
+  public static final class Request {
 
+    transient HttpRequest<?> http;
     final double longitude;
     final double latitude;
     final double vs30;
     final boolean truncate;
     final boolean maxdir;
 
-    RequestData(QueryParameters query) {
-      this.longitude = query.longitude;
-      this.latitude = query.latitude;
-      this.vs30 = query.vs30;
-      this.truncate = query.truncate;
-      this.maxdir = query.maxdir;
+    public Request(
+        HttpRequest<?> http,
+        double longitude,
+        double latitude,
+        int vs30,
+        boolean truncate,
+        boolean maxdir) {
+
+      this.http = http;
+      this.longitude = checkLongitude(longitude);
+      this.latitude = checkLatitude(latitude);
+      this.vs30 = checkInRange(Site.VS30_RANGE, Site.Key.VS30, vs30);
+      this.truncate = truncate;
+      this.maxdir = maxdir;
     }
   }
 
@@ -220,40 +201,21 @@ public final class HazardService {
     }
   }
 
-  private static String imtShortLabel(Imt imt) {
-    if (imt.equals(Imt.PGA) || imt.equals(Imt.PGV)) {
-      return imt.name();
-    } else if (imt.isSA()) {
-      return imt.period() + " s";
-    }
-    return imt.toString();
-  }
-
-  @Deprecated
-  static class RequestDataOld extends ServiceRequestData {
-    final double vs30;
-
-    RequestDataOld(Query query, double vs30) {
-      super(query);
-      this.vs30 = vs30;
-    }
-  }
-
-  private static final class ResponseData {
+  private static final class Response {
     final ResponseMetadata metadata;
-    final List<HazardResponse> hazardCurves;
+    final List<ImtCurves> hazardCurves;
 
-    ResponseData(ResponseMetadata metadata, List<HazardResponse> hazardCurves) {
+    Response(ResponseMetadata metadata, List<ImtCurves> hazardCurves) {
       this.metadata = metadata;
       this.hazardCurves = hazardCurves;
     }
   }
 
-  private static final class HazardResponse {
+  private static final class ImtCurves {
     final Parameter imt;
     final List<Curve> data;
 
-    HazardResponse(Imt imt, List<Curve> data) {
+    ImtCurves(Imt imt, List<Curve> data) {
       this.imt = new Parameter(imtShortLabel(imt), imt.name());
       this.data = data;
     }
@@ -273,9 +235,8 @@ public final class HazardService {
 
   private static final class ResultBuilder {
 
-    String url;
     Stopwatch timer;
-    RequestData request;
+    Request request;
 
     Map<Imt, Map<SourceType, MutableXySequence>> componentMaps;
     Map<Imt, MutableXySequence> totalMap;
@@ -311,23 +272,18 @@ public final class HazardService {
       return this;
     }
 
-    ResultBuilder url(HttpRequest<?> request) {
-      url = request.getUri().getPath();
-      return this;
-    }
-
     ResultBuilder timer(Stopwatch timer) {
       this.timer = timer;
       return this;
     }
 
-    ResultBuilder requestData(RequestData request) {
+    ResultBuilder request(Request request) {
       this.request = request;
       return this;
     }
 
-    ResponseBody<RequestData, ResponseData> build() {
-      var hazards = new ArrayList<HazardResponse>();
+    Response build() {
+      var hazards = new ArrayList<ImtCurves>();
 
       for (Imt imt : totalMap.keySet()) {
         var curves = new ArrayList<Curve>();
@@ -345,18 +301,15 @@ public final class HazardService {
               updateCurve(request, typeMap.get(type), imt)));
         }
 
-        hazards.add(new HazardResponse(imt, List.copyOf(curves)));
+        hazards.add(new ImtCurves(imt, List.copyOf(curves)));
       }
 
       Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer);
-      var response = new ResponseData(new ResponseMetadata(server), List.copyOf(hazards));
+      var response = new Response(
+          new ResponseMetadata(server),
+          List.copyOf(hazards));
 
-      return ResponseBody.<RequestData, ResponseData> success()
-          .name(NAME)
-          .url(url)
-          .request(request)
-          .response(response)
-          .build();
+      return response;
     }
   }
 
@@ -364,7 +317,7 @@ public final class HazardService {
 
   /* Convert to linear and possibly truncate and scale to max-direction. */
   private static XySequence updateCurve(
-      RequestData request,
+      Request request,
       XySequence curve,
       Imt imt) {
 
@@ -398,25 +351,13 @@ public final class HazardService {
     return limit;
   }
 
-  @Deprecated
-  public static class Query extends ServiceQueryData {
-    Integer vs30;
-
-    public Query(Double longitude, Double latitude, Integer vs30) {
-      super(longitude, latitude);
-      this.vs30 = vs30;
-    }
-
-    @Override
-    public boolean isNull() {
-      return super.isNull() && vs30 == null;
-    }
-
-    @Override
-    public void checkValues() {
-      super.checkValues();
-      WsUtils.checkValue(ServicesUtil.Key.VS30, vs30);
+  private static String imtShortLabel(Imt imt) {
+    if (imt.equals(Imt.PGA) || imt.equals(Imt.PGV)) {
+      return imt.name();
+    } else if (imt.isSA()) {
+      return imt.period() + " s";
     }
+    return imt.toString();
   }
 
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/MaxDirection.java b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/MaxDirection.java
similarity index 97%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/services/MaxDirection.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/hazard/MaxDirection.java
index e35c59355037416482937ce865aeee59400af2cf..99c40c42d3127fcca3bb176c6fb837d20d0d6c3d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/MaxDirection.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/hazard/MaxDirection.java
@@ -1,4 +1,4 @@
-package gov.usgs.earthquake.nshmp.www.services;
+package gov.usgs.earthquake.nshmp.www.hazard;
 
 import static gov.usgs.earthquake.nshmp.gmm.Imt.PGA;
 import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P01;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
index 1120be8504341bf5e2b843fb4b3615f2f9216869..ed8a2973d0cb7ce54e00d8d68d35dd70c4260c51 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
@@ -4,7 +4,7 @@ import com.google.common.base.Stopwatch;
 import com.google.common.base.Throwables;
 
 import gov.usgs.earthquake.nshmp.geo.Coordinates;
-import gov.usgs.earthquake.nshmp.www.services.ServletUtil;
+import gov.usgs.earthquake.nshmp.www.ServletUtil;
 
 /**
  * Service metadata, parameterization, and constraint strings, in JSON format.
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService2.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService2.java
deleted file mode 100644
index 63fb4261bd5c38b37aea9926f00c787e3cdbd1d1..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService2.java
+++ /dev/null
@@ -1,452 +0,0 @@
-package gov.usgs.earthquake.nshmp.www.services;
-
-import static com.google.common.base.Preconditions.checkState;
-import static gov.usgs.earthquake.nshmp.calc.HazardExport.curvesBySource;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
-
-import com.google.common.base.Stopwatch;
-
-import gov.usgs.earthquake.nshmp.calc.CalcConfig;
-import gov.usgs.earthquake.nshmp.calc.Hazard;
-import gov.usgs.earthquake.nshmp.calc.Site;
-import gov.usgs.earthquake.nshmp.data.MutableXySequence;
-import gov.usgs.earthquake.nshmp.data.XySequence;
-import gov.usgs.earthquake.nshmp.geo.Coordinates;
-import gov.usgs.earthquake.nshmp.geo.Location;
-import gov.usgs.earthquake.nshmp.gmm.Imt;
-import gov.usgs.earthquake.nshmp.model.HazardModel;
-import gov.usgs.earthquake.nshmp.model.SourceType;
-import gov.usgs.earthquake.nshmp.www.HazardController;
-import gov.usgs.earthquake.nshmp.www.ResponseBody;
-import gov.usgs.earthquake.nshmp.www.WsUtils;
-import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
-import gov.usgs.earthquake.nshmp.www.meta.Metadata;
-import gov.usgs.earthquake.nshmp.www.meta.Parameter;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
-
-import io.micronaut.http.HttpRequest;
-import io.micronaut.http.HttpResponse;
-import jakarta.inject.Singleton;
-
-/**
- * Probabilistic seismic hazard calculation handler for
- * {@link HazardController}.
- *
- * @author U.S. Geological Survey
- */
-@Singleton
-public final class HazardService2 {
-
-  private static final String NAME = "Hazard Service";
-
-  /** HazardController.doGetUsage() handler. */
-  public static HttpResponse<String> handleDoGetMetadata(HttpRequest<?> request) {
-    var url = request.getUri().getPath();
-    try {
-      var usage = new RequestMetadata(ServletUtil.model());// SourceServices.ResponseData();
-      var response = ResponseBody.usage()
-          .name(NAME)
-          .url(url)
-          .request(url)
-          .response(usage)
-          .build();
-      var svcResponse = ServletUtil.GSON.toJson(response);
-      return HttpResponse.ok(svcResponse);
-    } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, url);
-    }
-  }
-
-  /** HazardController.doGetHazard() handler. */
-  public static HttpResponse<String> handleDoGetHazard(
-      HttpRequest<?> request,
-      RequestData args) {
-
-    try {
-      // TODO still need to validate
-      // if (query.isEmpty()) {
-      // return handleDoGetUsage(urlHelper);
-      // }
-      // query.checkParameters();
-
-      // var data = new RequestData(query);
-
-      ResponseBody<RequestData, ResponseData> response = process(request, args);
-      String svcResponse = ServletUtil.GSON.toJson(response);
-      return HttpResponse.ok(svcResponse);
-
-    } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, request.getUri().getPath());
-    }
-  }
-
-  static ResponseBody<RequestData, ResponseData> process(
-      HttpRequest<?> request,
-      RequestData data) throws InterruptedException, ExecutionException {
-
-    var configFunction = new ConfigFunction();
-    var siteFunction = new SiteFunction(data);
-    var stopwatch = Stopwatch.createStarted();
-    var hazard = ServicesUtil.calcHazard(configFunction, siteFunction);
-
-    return new ResultBuilder()
-        .hazard(hazard)
-        .requestData(data)
-        .timer(stopwatch)
-        .url(request)
-        .build();
-  }
-
-  static class ConfigFunction implements Function<HazardModel, CalcConfig> {
-    @Override
-    public CalcConfig apply(HazardModel model) {
-      var configBuilder = CalcConfig.copyOf(model.config());
-      return configBuilder.build();
-    }
-  }
-
-  static class SiteFunction implements Function<CalcConfig, Site> {
-    final RequestData data;
-
-    private SiteFunction(RequestData data) {
-      this.data = data;
-    }
-
-    @Override // TODO this needs to pick up SiteData
-    public Site apply(CalcConfig config) {
-      return Site.builder()
-          .location(Location.create(data.longitude, data.latitude))
-          .vs30(data.vs30)
-          .build();
-    }
-  }
-
-  // public static class QueryParameters {
-  //
-  // final double longitude;
-  // final double latitude;
-  // final int vs30;
-  // final boolean truncate;
-  // final boolean maxdir;
-  //
-  // public QueryParameters(
-  // double longitude,
-  // double latitude,
-  // int vs30,
-  // boolean truncate,
-  // boolean maxdir) {
-  //
-  // this.longitude = longitude;
-  // this.latitude = latitude;
-  // this.vs30 = vs30;
-  // this.truncate = truncate;
-  // this.maxdir = maxdir;
-  // }
-  //
-  // // void checkParameters() {
-  // // checkParameter(longitude, "longitude");
-  // // checkParameter(latitude, "latitude");
-  // // checkParameter(vs30, "vs30");
-  // // }
-  // }
-
-  // private static void checkParameter(Object param, String id) {
-  // checkNotNull(param, "Missing parameter: %s", id);
-  // // TODO check range here
-  // }
-
-  /* Service request and model metadata */
-  static class RequestMetadata {
-
-    final SourceModel model;
-    final DoubleParameter longitude;
-    final DoubleParameter latitude;
-    final DoubleParameter vs30;
-
-    RequestMetadata(HazardModel model) {
-      this.model = new SourceModel(model);
-      // TODO need min max from model
-      longitude = new DoubleParameter(
-          "Longitude",
-          "°",
-          Coordinates.LON_RANGE.lowerEndpoint(),
-          Coordinates.LON_RANGE.upperEndpoint());
-
-      latitude = new DoubleParameter(
-          "Latitude",
-          "°",
-          Coordinates.LAT_RANGE.lowerEndpoint(),
-          Coordinates.LAT_RANGE.upperEndpoint());
-
-      vs30 = new DoubleParameter(
-          "Latitude",
-          "m/s",
-          150,
-          1500);
-    }
-  }
-
-  // static class RequestData {
-  //
-  // final double longitude;
-  // final double latitude;
-  // final double vs30;
-  // final boolean truncate;
-  // final boolean maxdir;
-  //
-  // RequestData(QueryParameters query) {
-  // this.longitude = query.longitude;
-  // this.latitude = query.latitude;
-  // this.vs30 = query.vs30;
-  // this.truncate = query.truncate;
-  // this.maxdir = query.maxdir;
-  // }
-  // }
-
-  private static final class ResponseMetadata {
-    final String xlabel = "Ground Motion (g)";
-    final String ylabel = "Annual Frequency of Exceedence";
-    final Object server;
-
-    ResponseMetadata(Object server) {
-      this.server = server;
-    }
-  }
-
-  private static String imtShortLabel(Imt imt) {
-    if (imt.equals(Imt.PGA) || imt.equals(Imt.PGV)) {
-      return imt.name();
-    } else if (imt.isSA()) {
-      return imt.period() + " s";
-    }
-    return imt.toString();
-  }
-
-  // @Deprecated
-  // static class RequestDataOld extends ServiceRequestData {
-  // final double vs30;
-  //
-  // RequestDataOld(Query query, double vs30) {
-  // super(query);
-  // this.vs30 = vs30;
-  // }
-  // }
-
-  private static final class ResponseData {
-    final ResponseMetadata metadata;
-    final List<HazardResponse> hazardCurves;
-
-    ResponseData(ResponseMetadata metadata, List<HazardResponse> hazardCurves) {
-      this.metadata = metadata;
-      this.hazardCurves = hazardCurves;
-    }
-  }
-
-  private static final class HazardResponse {
-    final Parameter imt;
-    final List<Curve> data;
-
-    HazardResponse(Imt imt, List<Curve> data) {
-      this.imt = new Parameter(imtShortLabel(imt), imt.name());
-      this.data = data;
-    }
-  }
-
-  private static final class Curve {
-    final String component;
-    final XySequence values;
-
-    Curve(String component, XySequence values) {
-      this.component = component;
-      this.values = values;
-    }
-  }
-
-  private static final String TOTAL_KEY = "Total";
-
-  private static final class ResultBuilder {
-
-    String url;
-    Stopwatch timer;
-    RequestData request;
-
-    Map<Imt, Map<SourceType, MutableXySequence>> componentMaps;
-    Map<Imt, MutableXySequence> totalMap;
-
-    ResultBuilder hazard(Hazard hazardResult) {
-      // TODO necessary??
-      checkState(totalMap == null, "Hazard has already been added to this builder");
-
-      componentMaps = new EnumMap<>(Imt.class);
-      totalMap = new EnumMap<>(Imt.class);
-
-      var typeTotalMaps = curvesBySource(hazardResult);
-
-      for (var imt : hazardResult.curves().keySet()) {
-
-        /* Total curve for IMT. */
-        XySequence.addToMap(imt, totalMap, hazardResult.curves().get(imt));
-
-        /* Source component curves for IMT. */
-        var typeTotalMap = typeTotalMaps.get(imt);
-        var componentMap = componentMaps.get(imt);
-
-        if (componentMap == null) {
-          componentMap = new EnumMap<>(SourceType.class);
-          componentMaps.put(imt, componentMap);
-        }
-
-        for (var type : typeTotalMap.keySet()) {
-          XySequence.addToMap(type, componentMap, typeTotalMap.get(type));
-        }
-      }
-
-      return this;
-    }
-
-    ResultBuilder url(HttpRequest<?> request) {
-      url = request.getUri().getPath();
-      return this;
-    }
-
-    ResultBuilder timer(Stopwatch timer) {
-      this.timer = timer;
-      return this;
-    }
-
-    ResultBuilder requestData(RequestData request) {
-      this.request = request;
-      return this;
-    }
-
-    ResponseBody<RequestData, ResponseData> build() {
-      var hazards = new ArrayList<HazardResponse>();
-
-      for (Imt imt : totalMap.keySet()) {
-        var curves = new ArrayList<Curve>();
-
-        // total curve
-        curves.add(new Curve(
-            TOTAL_KEY,
-            updateCurve(request, totalMap.get(imt), imt)));
-
-        // component curves
-        var typeMap = componentMaps.get(imt);
-        for (SourceType type : typeMap.keySet()) {
-          curves.add(new Curve(
-              type.toString(),
-              updateCurve(request, typeMap.get(type), imt)));
-        }
-
-        hazards.add(new HazardResponse(imt, List.copyOf(curves)));
-      }
-
-      Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer);
-      var response = new ResponseData(new ResponseMetadata(server), List.copyOf(hazards));
-
-      return ResponseBody.<RequestData, ResponseData> success()
-          .name(NAME)
-          .url(url)
-          .request(request)
-          .response(response)
-          .build();
-    }
-  }
-
-  private static final double TRUNCATION_LIMIT = 1e-4;
-
-  /* Convert to linear and possibly truncate and scale to max-direction. */
-  private static XySequence updateCurve(
-      RequestData request,
-      XySequence curve,
-      Imt imt) {
-
-    /*
-     * If entire curve is <1e-4, this method will return a curve consisting of
-     * just the first point in the supplied curve.
-     *
-     * TODO We probably want to move the TRUNCATION_LIMIT out to a config.
-     */
-
-    double[] yValues = curve.yValues().toArray();
-    int limit = request.truncate ? truncationLimit(yValues) : yValues.length;
-    yValues = Arrays.copyOf(yValues, limit);
-
-    double scale = request.maxdir ? MaxDirection.FACTORS.get(imt) : 1.0;
-    double[] xValues = curve.xValues()
-        .limit(yValues.length)
-        .map(Math::exp)
-        .map(x -> x * scale)
-        .toArray();
-
-    return XySequence.create(xValues, yValues);
-  }
-
-  private static int truncationLimit(double[] yValues) {
-    int limit = 1;
-    double y = yValues[0];
-    while (y > TRUNCATION_LIMIT && limit < yValues.length) {
-      y = yValues[limit++];
-    }
-    return limit;
-  }
-
-  @Deprecated
-  public static class Query extends ServiceQueryData {
-    Integer vs30;
-
-    public Query(Double longitude, Double latitude, Integer vs30) {
-      super(longitude, latitude);
-      this.vs30 = vs30;
-    }
-
-    @Override
-    public boolean isNull() {
-      return super.isNull() && vs30 == null;
-    }
-
-    @Override
-    public void checkValues() {
-      super.checkValues();
-      WsUtils.checkValue(ServicesUtil.Key.VS30, vs30);
-    }
-  }
-
-  public static final class RequestData {
-
-    final double longitude;
-    final double latitude;
-    final int vs30;
-    final boolean truncate;
-    final boolean maxdir;
-
-    public RequestData(
-        double longitude,
-        double latitude,
-        int vs30,
-        boolean truncate,
-        boolean maxdir) {
-
-      this.longitude = longitude;
-      this.latitude = latitude;
-      this.vs30 = vs30;
-      this.truncate = truncate;
-      this.maxdir = maxdir;
-    }
-
-    // void checkParameters() {
-    // checkParameter(longitude, "longitude");
-    // checkParameter(latitude, "latitude");
-    // checkParameter(vs30, "vs30");
-    // }
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
index 0312e7359a0af379d43fdd55c8a5903edce67a2a..8c1a50c43132cf4160bd38b4be3f6a0fb9076e8b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
@@ -17,14 +17,15 @@ import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.model.HazardModel;
 import gov.usgs.earthquake.nshmp.www.RateController;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil;
+import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.WsUtils;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil.ServiceQueryData;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata.DefaultParameters;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
-
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
 import jakarta.inject.Singleton;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceLogicTreesService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceLogicTreesService.java
index 3d0dfe0a7055376d3b5e764e252f23ca80f4f265..4a7882cdbc8bb789d79b79e1f03aadfaba63ee2b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceLogicTreesService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceLogicTreesService.java
@@ -2,6 +2,8 @@ package gov.usgs.earthquake.nshmp.www.services;
 
 import gov.usgs.earthquake.nshmp.model.Models;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil;
+import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.SourceLogicTreesController;
 
 import io.micronaut.http.HttpRequest;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
index 83910b2fc623cd7b27e81855da5263435cb87d65..bd22e1c37247c832f4265305b80268f7aeb613a8 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
@@ -12,9 +12,10 @@ import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
 import gov.usgs.earthquake.nshmp.model.HazardModel;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
+import gov.usgs.earthquake.nshmp.www.ServicesUtil;
+import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.WsUtils;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
-
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
 import jakarta.inject.Singleton;
@@ -105,7 +106,7 @@ public class SourceServices {
     Set<Gmm> gmms;
     Map<NehrpSiteClass, Double> siteClasses;
 
-    SourceModel(HazardModel model) {
+    public SourceModel(HazardModel model) {
       name = model.name();
       gmms = model.gmms();
       siteClasses = model.siteClasses();