diff --git a/src/aashto/build.gradle b/src/aashto/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..747bdaf3abdb2fccdeb72b6c9154a6b395fdb254
--- /dev/null
+++ b/src/aashto/build.gradle
@@ -0,0 +1,4 @@
+
+dependencies {
+  implementation project(":src:lib")
+}
diff --git a/src/aashto/openapi.properties b/src/aashto/openapi.properties
new file mode 100644
index 0000000000000000000000000000000000000000..75e362540a617f22719aa540e333d8a469f33b0b
--- /dev/null
+++ b/src/aashto/openapi.properties
@@ -0,0 +1 @@
+micronaut.openapi.target.file = build/classes/java/main/META-INF/swagger/nshmp-ws-static.yml
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a9c943b3b74a5f31ff24cbf626dcb54537f0359
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java
@@ -0,0 +1,62 @@
+package gov.usgs.earthquake.nshmp.netcdf;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import gov.usgs.earthquake.nshmp.data.XySequence;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.data.BoundingData;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
+import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
+import gov.usgs.earthquake.nshmp.netcdf.reader.BoundingReaderGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.reader.ReaderGroundMotions;
+
+import ucar.nc2.dataset.NetcdfDatasets;
+
+/**
+ * NetCDF data for ground motions.
+ *
+ * @author U.S. Geological Survey
+ */
+public class NetcdfGroundMotions extends Netcdf<XySequence> {
+
+  public NetcdfGroundMotions(Path netcdfPath) {
+    super(netcdfPath);
+  }
+
+  @Override
+  public BoundingData<XySequence> boundingData(Location site) {
+    return new BoundingReaderGroundMotions(this, site).boundingData();
+  }
+
+  @Override
+  public NetcdfData netcdfData() {
+    return netcdfData;
+  }
+
+  @Override
+  public StaticData<XySequence> staticData(Location site) {
+    return boundingData(site).get(site);
+  }
+
+  @Override
+  public XySequence staticData(Location site, NehrpSiteClass siteClass) {
+    checkArgument(
+        netcdfData.siteClasses().contains(siteClass),
+        "Site class [" + siteClass + "] not supported");
+    return staticData(site).get(siteClass);
+  }
+
+  @Override
+  ReaderGroundMotions getNetcdfData(Path netcdfPath) {
+    try (var ncd = NetcdfDatasets.openDataset(netcdfPath.toString())) {
+      var group = ncd.getRootGroup();
+      return new ReaderGroundMotions(group);
+    } catch (IOException e) {
+      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + " ]");
+    }
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingReaderGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingReaderGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..1923bfd08d08719360b9832e3a879fbdb5bc35fa
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingReaderGroundMotions.java
@@ -0,0 +1,194 @@
+package gov.usgs.earthquake.nshmp.netcdf.reader;
+
+import java.io.IOException;
+import java.util.List;
+
+import gov.usgs.earthquake.nshmp.data.XySequence;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.netcdf.Netcdf;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.data.BoundingData;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape.IndexKey;
+import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
+
+import ucar.ma2.Array;
+import ucar.ma2.DataType;
+import ucar.ma2.InvalidRangeException;
+import ucar.nc2.dataset.NetcdfDatasets;
+
+/**
+ * Creates the bounding locations and data associated with a specific site for
+ * ground motions.
+ *
+ * @author U.S. Geological Survey
+ */
+public class BoundingReaderGroundMotions extends BoundingReader<XySequence> {
+
+  public static final double PGA_VALUE = 0.001;
+  public static final double PGV_VALUE = 0.0001;
+
+  public BoundingReaderGroundMotions(NetcdfGroundMotions netcdf, Location site) {
+    super(netcdf, site);
+  }
+
+  @Override
+  StaticData<XySequence> calculateTargetData(
+      List<BoundingLocation> boundingLocations,
+      BoundingData<XySequence> boundingData,
+      double fracLon,
+      double fracLat) {
+    var westTarget = getTargetData(
+        boundingData.get(boundingLocations.get(0).location),
+        boundingData.get(boundingLocations.get(1).location),
+        fracLat);
+
+    var eastTarget = getTargetData(
+        boundingData.get(boundingLocations.get(3).location),
+        boundingData.get(boundingLocations.get(2).location),
+        fracLat);
+
+    return getTargetData(westTarget, eastTarget, fracLon);
+  }
+
+  @Override
+  BoundingData<XySequence> extractDataAt(
+      Netcdf<XySequence> netcdf,
+      List<BoundingLocation> boundingLocations,
+      int idxLonLL,
+      int idxLatLL) {
+    try (var ncd = NetcdfDatasets.openDataset(netcdf.netcdfPath().toString())) {
+      var netcdfData = netcdf.netcdfData();
+      var boundingData = BoundingData.<XySequence> builder();
+      var targetGroup = ncd.getRootGroup();
+      var netcdfShape = netcdf.netcdfShape();
+
+      // Build origin array, e.g. [lon, lat, 0, 0]
+      var targetOrigin = netcdfShape.buildShape()
+          .add(IndexKey.LATITUDE, idxLatLL)
+          .add(IndexKey.LONGITUDE, idxLonLL)
+          .build();
+
+      // Build target shape array, e.g. [2, 2, nImt, nSiteClass]
+      var targetShape = netcdfShape.buildShape()
+          .add(IndexKey.SITE_CLASS, netcdfData.siteClasses().size())
+          .add(IndexKey.LATITUDE, 2)
+          .add(IndexKey.LONGITUDE, 2)
+          .add(IndexKey.IMT, netcdfData.imts().size())
+          .build();
+
+      var groundMotionArray = targetGroup
+          .findVariableLocal(Key.GROUND_MOTION)
+          .read(targetOrigin, targetShape);
+
+      // Main data shape, e.g. [1, 1, nImt, nSiteClass]
+      var shape = netcdfShape.buildShape()
+          .add(IndexKey.SITE_CLASS, netcdfData.siteClasses().size())
+          .add(IndexKey.LATITUDE, 1)
+          .add(IndexKey.LONGITUDE, 1)
+          .add(IndexKey.IMT, netcdfData.imts().size())
+          .build();
+
+      for (var boundingLocation : boundingLocations) {
+        boundingData.put(
+            boundingLocation.location,
+            mapDataFromArray(netcdf, groundMotionArray.section(boundingLocation.origin, shape)));
+      }
+
+      return boundingData.build();
+    } catch (IOException | InvalidRangeException e) {
+      throw new RuntimeException(
+          "Could not read Netcdf file [" + netcdf.netcdfPath() + "]. " + e.getMessage());
+    }
+  }
+
+  @Override
+  StaticData<XySequence> getTargetData(
+      StaticData<XySequence> d1,
+      StaticData<XySequence> d2,
+      double frac) {
+    NetcdfUtils.checkBoundingGroundMotion(d1, d2);
+    return frac == 0.0 ? d1
+        : frac == 1.0 ? d2 : NetcdfUtils.linearInterpolateGroundMotions(d1, d2, frac);
+  }
+
+  @Override
+  StaticData<XySequence> mapDataFromArray(
+      Netcdf<XySequence> netcdf,
+      Array array) {
+    var netcdfData = netcdf.netcdfData();
+    var staticData = StaticData.<XySequence> builder();
+
+    for (int iSiteClass = 0; iSiteClass < netcdfData.siteClasses().size(); iSiteClass++) {
+      var siteClass = netcdfData.siteClasses().get(iSiteClass);
+
+      var imts = netcdfData.imts();
+      var periods = imts.stream()
+          .mapToDouble(imt -> {
+            if (imt == Imt.PGA) {
+              return PGA_VALUE;
+            } else if (imt == Imt.PGV) {
+              return PGV_VALUE;
+            }
+            return imt.period();
+          })
+          .toArray();
+
+      // Build origin array, e.g [0, siteClass]
+      var origin = netcdf.netcdfShape().buildShape()
+          .add(IndexKey.IMT, 0)
+          .add(IndexKey.SITE_CLASS, iSiteClass)
+          .reduce()
+          .build();
+
+      // Build shape array, e.g. [nImts, 1]
+      var shape = netcdf.netcdfShape().buildShape()
+          .add(IndexKey.IMT, imts.size())
+          .add(IndexKey.SITE_CLASS, 1)
+          .reduce()
+          .build();
+
+      try {
+        var xySequence = XySequence.create(
+            periods,
+            (double[]) array.section(origin, shape).reduce().get1DJavaArray(DataType.DOUBLE));
+
+        staticData.put(siteClass, xySequence);
+      } catch (InvalidRangeException e) {
+        throw new RuntimeException(e.getMessage());
+      }
+    }
+
+    return staticData.build();
+  }
+
+  @Override
+  BoundingData<XySequence> setBoundingData(
+      Netcdf<XySequence> netcdf,
+      Location site,
+      List<BoundingLocation> boundingLocations) {
+    var netcdfGroundMotions = (NetcdfGroundMotions) netcdf;
+    var netcdfData = netcdfGroundMotions.netcdfData();
+    var longitudes = netcdfData.longitudes();
+    var latitudes = netcdfData.latitudes();
+    var idxLonLL = NetcdfUtils.getIdxLTEQ(longitudes, site.longitude);
+    var idxLatLL = NetcdfUtils.getIdxLTEQ(latitudes, site.latitude);
+
+    var groundMotions =
+        extractDataAt(netcdfGroundMotions, boundingLocations, idxLonLL, idxLatLL);
+    var fracLon = NetcdfUtils.calcGridFrac(longitudes, idxLonLL, site.longitude);
+    var fracLat = NetcdfUtils.calcGridFrac(latitudes, idxLatLL, site.latitude);
+
+    var builder = BoundingData.<XySequence> builder();
+    groundMotions.forEach((key, value) -> builder.put(key, value));
+    builder.put(
+        site,
+        calculateTargetData(boundingLocations, groundMotions, fracLon, fracLat))
+        .build();
+    var boundingData = builder.build();
+
+    NetcdfUtils.checkBoundingGroundMotions(boundingData, boundingLocations.get(0).location);
+    return boundingData;
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..f45da139d717469be4fc52160892913a4d392736
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java
@@ -0,0 +1,32 @@
+package gov.usgs.earthquake.nshmp.netcdf.reader;
+
+import java.io.IOException;
+
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape.IndexKey;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
+
+import ucar.nc2.Group;
+
+/**
+ * Read in ground motions NetCDF files
+ *
+ * @author U.S. Geological Survey
+ */
+public class ReaderGroundMotions extends Reader {
+
+  public ReaderGroundMotions(Group targetGroup) throws IOException {
+    super(targetGroup);
+  }
+
+  @Override
+  NetcdfShape buildNetcdfShape(Group group) {
+    var vData = group.findVariableLocal(Key.GROUND_MOTION);
+    return NetcdfShape.builder()
+        .add(IndexKey.IMT, vData.findDimensionIndex(Key.IMT))
+        .add(IndexKey.LATITUDE, vData.findDimensionIndex(Key.LAT))
+        .add(IndexKey.LONGITUDE, vData.findDimensionIndex(Key.LON))
+        .add(IndexKey.SITE_CLASS, vData.findDimensionIndex(Key.SITE_CLASS))
+        .build();
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Application.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Application.java
new file mode 100644
index 0000000000000000000000000000000000000000..85cdcf7d29737c2ec14c04923e168bad6666c0ab
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Application.java
@@ -0,0 +1,21 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import io.micronaut.runtime.Micronaut;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
+
+@OpenAPIDefinition(
+    info = @Info(
+        title = "AASHTO Ground Motion Data",
+        description = "### Get static curves\n" +
+            "See the service usage for supported longitudes, " +
+            "latitudes, and site classes"))
+public class Application {
+
+  public static void main(String[] args) {
+    Micronaut.build(args)
+        .mainClass(Application.class)
+        .start();
+  }
+
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d3fa24ef6e36f13ecbead81df6d139406fd0d4b
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
@@ -0,0 +1,135 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.nio.file.Path;
+
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
+import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
+
+import io.micronaut.context.annotation.Value;
+import io.micronaut.context.event.StartupEvent;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.QueryValue;
+import io.micronaut.runtime.event.annotation.EventListener;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.inject.Inject;
+
+/**
+ * Micronaut controller for getting static hazards or ground motions from a
+ * NetCDF file.
+ *
+ * @see NetcdfService
+ *
+ * @author U.S. Geological Survey
+ */
+@Tag(name = "Static Ground Motion Data")
+@Controller("/ground-motions")
+public class NetcdfController {
+
+  @Inject
+  private NshmpMicronautServlet servlet;
+
+  @Value("${nshmp-ws-static.netcdf-file}")
+  Path netcdfPath;
+
+  NetcdfServiceGroundMotions service;
+
+  /**
+   * Read in data type and return the appropriate service to use.
+   */
+  @EventListener
+  void startup(StartupEvent event) {
+    var netcdfHazard = new NetcdfGroundMotions(netcdfPath);
+    service = new NetcdfServiceGroundMotions(netcdfHazard);
+  }
+
+  /**
+   * GET method to return a static curve using URL query.
+   *
+   * @param request The HTTP request
+   * @param longitude The longitude of the site
+   * @param latitude Latitude of the site
+   * @param siteClass The site class (optional)
+   */
+  @Operation(
+      summary = "Returns curve(s) given a longitude, latitude, and/or a site class",
+      description = "Retrieve static curve(s) from a NSHM NetCDF file.\n\n" +
+          "For supported longitudes, latitudes, and site classes see the usage information.",
+      operationId = "netcdf_data_doGetHazard")
+  @ApiResponse(
+      description = "Returns a static curve from the NetCDF file",
+      responseCode = "200",
+      content = @Content(
+          schema = @Schema(type = "string")))
+  @Get(uri = "{?longitude,latitude,siteClass}", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGet(
+      HttpRequest<?> request,
+      @Schema(required = true) @QueryValue @Nullable Double longitude,
+      @Schema(required = true) @QueryValue @Nullable Double latitude,
+      @QueryValue @Nullable NehrpSiteClass siteClass) {
+    var query = new Query(longitude, latitude, siteClass);
+    return service.handleServiceCall(request, query);
+  }
+
+  /**
+   * GET method to return static curves using slash delimited.
+   *
+   * @param request The HTTP request
+   * @param longitude The longitude of the site
+   * @param latitude Latitude of the site
+   * @param siteClass The site class
+   */
+  @Operation(
+      summary = "Returns static curves given a longitude, latitude, and site class.",
+      description = "Retrieve static curves from a NetCDF file.\n\n" +
+          "For supported longitudes, latitudes, and site classes see the usage information.",
+      operationId = "netcdf_data_doGetHazardSlashWithSiteClass")
+  @ApiResponse(
+      description = "Returns static curves from the NetCDF file",
+      responseCode = "200",
+      content = @Content(
+          schema = @Schema(type = "string")))
+  @Get(uri = "/{longitude}/{latitude}/{siteClass}", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGetSlashBySite(
+      HttpRequest<?> request,
+      @Schema(required = true) @PathVariable @Nullable Double longitude,
+      @Schema(required = true) @PathVariable @Nullable Double latitude,
+      @Schema(required = true) @PathVariable @Nullable NehrpSiteClass siteClass) {
+    return doGet(request, longitude, latitude, siteClass);
+  }
+
+  /**
+   * GET method to return hazard curves using slash delimited.
+   *
+   * @param request The HTTP request
+   * @param longitude The longitude of the site
+   * @param latitude Latitude of the site
+   */
+  @Operation(
+      summary = "Returns static curves given a longitude and latitude.",
+      description = "Retrieve static curves from a NetCDF file.\n\n" +
+          "For supported longitudes and latitudes see the usage information.",
+      operationId = "netcdf_data_doGetHazardSlash")
+  @ApiResponse(
+      description = "Returns static curves from the NSHM NetCDF file",
+      responseCode = "200",
+      content = @Content(
+          schema = @Schema(type = "string")))
+  @Get(uri = "/{longitude}/{latitude}", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGetSlash(
+      HttpRequest<?> request,
+      @Schema(required = true) @PathVariable @Nullable Double longitude,
+      @Schema(required = true) @PathVariable @Nullable Double latitude) {
+    return doGet(request, longitude, latitude, null);
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e045cc5d527ba560e5724619f076bcfec3d382d
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java
@@ -0,0 +1,135 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import gov.usgs.earthquake.nshmp.data.XySequence;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
+import gov.usgs.earthquake.nshmp.netcdf.reader.BoundingReaderGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key;
+import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service;
+import gov.usgs.earthquake.nshmp.www.Response;
+import gov.usgs.earthquake.nshmp.www.WsUtils;
+import gov.usgs.earthquake.nshmp.www.meta.Status;
+
+import io.micronaut.http.HttpRequest;
+
+/**
+ * Handles service calls for ground motions.
+ *
+ * @see NetcdfController
+ *
+ * @author U.S. Geological Survey
+ */
+public class NetcdfServiceGroundMotions extends NetcdfService {
+
+  static final String SERVICE_DESCRIPTION = "Get static ground motions from a NetCDF file";
+  static final String SERVICE_NAME = "Static Ground Motions";
+  static final String X_LABEL = "Period (s)";
+  static final String Y_LABEL = "Median Ground Motion (g)";
+
+  public NetcdfServiceGroundMotions(NetcdfGroundMotions netcdf) {
+    super(netcdf);
+  }
+
+  @Override
+  Response<String, Metadata> getMetadataResponse(HttpRequest<?> request) {
+    var metadata = new Metadata(request, SERVICE_DESCRIPTION);
+    var url = request.getUri().toString();
+    return new Response<>(Status.USAGE, SERVICE_NAME, url, metadata, url);
+  }
+
+  @Override
+  String getServiceName() {
+    return SERVICE_NAME;
+  }
+
+  @Override
+  NetcdfGroundMotions netcdf() {
+    return (NetcdfGroundMotions) netcdf;
+  }
+
+  @Override
+  Response<?, ?> processRequest(HttpRequest<?> httpRequest, Query query, Service service) {
+    var site = Location.create(query.longitude, query.latitude);
+    var requestData = new RequestData(site);
+    var url = httpRequest.getUri().toString();
+
+    switch (service) {
+      case CURVES:
+        return processCurves(requestData, url);
+      case CURVES_BY_SITE_CLASS:
+        requestData = new RequestDataSiteClass(site, query.siteClass);
+        return processCurvesSiteClass(
+            (RequestDataSiteClass) requestData, url);
+      default:
+        throw new RuntimeException("Netcdf service [" + service + "] not found");
+    }
+  }
+
+  @Override
+  Response<RequestDataSiteClass, ResponseData<ResponseMetadata>> processCurvesSiteClass(
+      RequestDataSiteClass request,
+      String url) {
+    WsUtils.checkValue(Key.LATITUDE, request.site.latitude);
+    WsUtils.checkValue(Key.LONGITUDE, request.site.longitude);
+    WsUtils.checkValue(Key.SITE_CLASS, request.siteClass);
+    var curves = netcdf().staticData(request.site, request.siteClass);
+    var responseData = toResponseData(request, curves);
+    return new Response<>(Status.SUCCESS, SERVICE_NAME, request, responseData, url);
+  }
+
+  @Override
+  Response<RequestData, List<ResponseData<ResponseMetadata>>> processCurves(RequestData request,
+      String url) {
+    WsUtils.checkValue(Key.LATITUDE, request.site.latitude);
+    WsUtils.checkValue(Key.LONGITUDE, request.site.longitude);
+    var curves = netcdf().staticData(request.site);
+    var responseData = toList(request.site, curves);
+    return new Response<>(Status.SUCCESS, SERVICE_NAME, request, responseData, url);
+  }
+
+  List<ResponseData<ResponseMetadata>> toList(
+      Location site,
+      StaticData<XySequence> curves) {
+    return curves.entrySet().stream()
+        .map(entry -> {
+          var request = new RequestDataSiteClass(site, entry.getKey());
+          return toResponseData(request, entry.getValue());
+        })
+        .collect(Collectors.toList());
+  }
+
+  ResponseData<ResponseMetadata> toResponseData(
+      RequestDataSiteClass request,
+      XySequence curves) {
+    var metadata = new ResponseMetadataGroundMotions(
+        X_LABEL,
+        Y_LABEL,
+        request.site,
+        request.siteClass);
+    return new ResponseData<>(metadata, curves);
+  }
+
+  static class ResponseMetadataGroundMotions extends ResponseMetadata {
+    Map<String, Double> imtValues;
+
+    ResponseMetadataGroundMotions(
+        String xLabel,
+        String yLabel,
+        Location site,
+        NehrpSiteClass siteClass) {
+      super(xLabel, yLabel, site, siteClass);
+      imtValues = Map.of(
+          Imt.PGA.name(),
+          BoundingReaderGroundMotions.PGA_VALUE,
+          Imt.PGV.name(),
+          BoundingReaderGroundMotions.PGV_VALUE);
+    }
+  }
+}
diff --git a/src/aashto/src/main/resources/aashto-example.nc b/src/aashto/src/main/resources/aashto-example.nc
new file mode 100644
index 0000000000000000000000000000000000000000..0b4e8d883e15107bd60ed7fc8bde3a9da0066427
Binary files /dev/null and b/src/aashto/src/main/resources/aashto-example.nc differ
diff --git a/src/aashto/src/main/resources/application.yml b/src/aashto/src/main/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a6bdb5f70547e8371b49cd2d8e491ed617f5f259
--- /dev/null
+++ b/src/aashto/src/main/resources/application.yml
@@ -0,0 +1,14 @@
+micronaut:
+  io:
+    watch:
+      paths: src
+      restart: true
+  router:
+    static-resources:
+      swagger:
+        enabled: true
+        paths: classpath:swagger
+        mapping: /**
+
+nshmp-ws-static:
+  netcdf-file: ${netcdf:src/main/resources/aashto-example.nc}
diff --git a/src/aashto/src/main/resources/logback.xml b/src/aashto/src/main/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f74e41693090ae1b24cabe0642f4ec814564c4a1
--- /dev/null
+++ b/src/aashto/src/main/resources/logback.xml
@@ -0,0 +1,17 @@
+<configuration>
+
+  <appender name="STDOUT"
+    class="ch.qos.logback.core.ConsoleAppender">
+    <withJansi>true</withJansi>
+    <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder
+      by default -->
+    <encoder>
+      <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread])
+        %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="info">
+    <appender-ref ref="STDOUT" />
+  </root>
+</configuration>