diff --git a/projects/nshmp-apps/src/app/source/rates/app.component.ts b/projects/nshmp-apps/src/app/source/rates/app.component.ts
index cbb78b382184643054e18abfc2db33bc6f460440..e535b749916710247cac5017b98ff36e89cf2d14 100644
--- a/projects/nshmp-apps/src/app/source/rates/app.component.ts
+++ b/projects/nshmp-apps/src/app/source/rates/app.component.ts
@@ -1,4 +1,4 @@
-import {Component} from '@angular/core';
+import {Component, OnInit} from '@angular/core';
 import {NshmpLibNgAboutPageComponent} from '@ghsc/nshmp-lib-ng/about';
 import {NshmpLibNgHazardProvisionalModelComponent} from '@ghsc/nshmp-lib-ng/hazard';
 import {NshmpLibNgTemplateComponent} from '@ghsc/nshmp-lib-ng/nshmp';
@@ -15,6 +15,7 @@ import {AboutComponent} from './components/about/about.component';
 import {ContentComponent} from './components/content/content.component';
 import {ControlPanelComponent} from './components/control-panel/control-panel.component';
 import {PlotSettingsPanelComponent} from './components/plot-settings-panel/plot-settings-panel.component';
+import {AppService} from './services/app.service';
 
 @Component({
   imports: [
@@ -35,9 +36,15 @@ import {PlotSettingsPanelComponent} from './components/plot-settings-panel/plot-
   styleUrl: './app.component.scss',
   templateUrl: './app.component.html',
 })
-export class AppComponent {
+export class AppComponent implements OnInit {
   /** Navigation list for menu */
   navigationList = navigation();
   /** Application title */
   title = apps().source.rateAndProbability.display;
+
+  constructor(private service: AppService) {}
+
+  ngOnInit(): void {
+    this.service.init();
+  }
 }
diff --git a/projects/nshmp-apps/src/app/source/rates/models/control-form.model.ts b/projects/nshmp-apps/src/app/source/rates/models/control-form.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..901e600ee7ec617af30eaacd0a8acad16dd68115
--- /dev/null
+++ b/projects/nshmp-apps/src/app/source/rates/models/control-form.model.ts
@@ -0,0 +1,9 @@
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
+
+export interface ControlForm {
+  distance: number;
+  latitude: number;
+  longitude: number;
+  model: NshmId;
+  timespan: number;
+}
diff --git a/projects/nshmp-apps/src/app/source/rates/models/plots.model.ts b/projects/nshmp-apps/src/app/source/rates/models/plots.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dc6c671ce0983e2d283192a37a17f31c55adad5f
--- /dev/null
+++ b/projects/nshmp-apps/src/app/source/rates/models/plots.model.ts
@@ -0,0 +1,6 @@
+import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+
+export interface RatePlots {
+  probability: NshmpPlot;
+  rate: NshmpPlot;
+}
diff --git a/projects/nshmp-apps/src/app/source/rates/models/state.model.ts b/projects/nshmp-apps/src/app/source/rates/models/state.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..852559b3884bce3fd51891180cc760af3e7d6ab8
--- /dev/null
+++ b/projects/nshmp-apps/src/app/source/rates/models/state.model.ts
@@ -0,0 +1,22 @@
+import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
+import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
+import {ProbabilityUsage} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/probability-service';
+import {RateResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/rates-service';
+import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
+
+import {RatePlots} from './plots.model';
+
+export interface AppState {
+  /** Available NSHMs */
+  availableModels: Parameter[];
+  /** NSHM service metadata */
+  nshmServices: NshmMetadata[];
+  /** Hazard plots */
+  plots: RatePlots;
+  /** Service call info */
+  serviceCallInfo: ServiceCallInfo;
+  /** Rate service responses */
+  serviceResponse: RateResponse;
+  /** Rate usage responses */
+  usageResponses: Map<string, ProbabilityUsage>;
+}
diff --git a/projects/nshmp-apps/src/app/source/rates/services/app.service.ts b/projects/nshmp-apps/src/app/source/rates/services/app.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9d48be5055c1a5c95fbec32b10015e9c115714f8
--- /dev/null
+++ b/projects/nshmp-apps/src/app/source/rates/services/app.service.ts
@@ -0,0 +1,170 @@
+import {computed, Injectable, Signal, signal} from '@angular/core';
+import {FormBuilder} from '@angular/forms';
+import {HazardService} from '@ghsc/nshmp-lib-ng/hazard';
+import {NshmpService, SpinnerService} from '@ghsc/nshmp-lib-ng/nshmp';
+import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
+import {
+  ProbabilityRequestMetadata,
+  ProbabilityUsage,
+} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/probability-service';
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
+import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
+import deepEqual from 'deep-equal';
+import {environment} from 'projects/nshmp-apps/src/environments/environment';
+import {AppServiceModel} from 'projects/nshmp-apps/src/shared/models/app-service.model';
+import {SharedService} from 'projects/nshmp-apps/src/shared/services/shared.service';
+import {catchError} from 'rxjs';
+
+import {ControlForm} from '../models/control-form.model';
+import {RatePlots} from '../models/plots.model';
+import {AppState} from '../models/state.model';
+
+@Injectable({
+  providedIn: 'root',
+})
+export class AppService
+  extends SharedService
+  implements AppServiceModel<AppState, ControlForm>
+{
+  /** nshmp-haz-ws web config */
+  private nshmpHazWs = environment.webServices.nshmpHazWs;
+  /** Probability endpoint */
+  private probabilityEndpoint =
+    this.nshmpHazWs.services.curveServices.probability;
+  /** Rate endpoint */
+  private rateEndpoint = this.nshmpHazWs.services.curveServices.rate;
+
+  state = signal<AppState>(this.initialState());
+  formGroup = this.formBuilder.group<ControlForm>(this.defaultFormValues());
+
+  constructor(
+    private formBuilder: FormBuilder,
+    private nshmpService: NshmpService,
+    private spinnerService: SpinnerService,
+    private hazardService: HazardService
+  ) {
+    super();
+    this.addValidators();
+  }
+
+  /**
+   * Returns the available models.
+   */
+  get availableModels(): Signal<Parameter[]> {
+    return computed(() => this.state().availableModels);
+  }
+
+  /**
+   * Returns the metadata of the NSHM observable.
+   */
+  get nshmService(): Signal<NshmMetadata> {
+    return computed(() =>
+      this.state().nshmServices.find(
+        nshmService => nshmService.model === this.formGroup.getRawValue().model
+      )
+    );
+  }
+
+  /**
+   * Return the usage for the selected model.
+   */
+  get usage(): Signal<ProbabilityUsage> {
+    return computed(() =>
+      this.state().usageResponses?.get(this.formGroup.getRawValue().model)
+    );
+  }
+
+  addValidators(): void {
+    const controls = this.formGroup.controls;
+
+    controls.distance.addValidators([this.validateRequired()]);
+    controls.latitude.addValidators([
+      this.validateRequired(),
+      this.validateNan(),
+    ]);
+    controls.longitude.addValidators([
+      this.validateRequired(),
+      this.validateNan(),
+    ]);
+    controls.timespan.addValidators([this.validateRequired()]);
+  }
+
+  callService(): void {
+    throw new Error('Method not implemented.');
+  }
+
+  defaultFormValues(): ControlForm {
+    return {
+      distance: null,
+      latitude: null,
+      longitude: null,
+      model: NshmId.CONUS_2018,
+      timespan: null,
+    };
+  }
+
+  init(): void {
+    const spinnerRef = this.spinnerService.show(
+      SpinnerService.MESSAGE_METADATA
+    );
+
+    this.hazardService
+      .dynamicNshms$<ProbabilityRequestMetadata>(
+        `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
+        this.probabilityEndpoint
+      )
+      .pipe(
+        catchError((error: Error) => {
+          spinnerRef.close();
+          return this.nshmpService.throwError$(error);
+        })
+      )
+      .subscribe(({models, nshmServices, usageResponses}) => {
+        spinnerRef.close();
+
+        this.updateState({
+          availableModels: models,
+          nshmServices,
+          usageResponses,
+        });
+
+        console.log(this.state());
+      });
+  }
+
+  initialState(): AppState {
+    return {
+      availableModels: [],
+      nshmServices: [],
+      plots: this.defaultPlots(),
+      serviceCallInfo: {
+        serviceCalls: [],
+        serviceName: '',
+        usage: [],
+      },
+      serviceResponse: null,
+      usageResponses: null,
+    };
+  }
+
+  updateState(state: Partial<AppState>): void {
+    const updatedState = {
+      ...this.state(),
+      ...state,
+    };
+
+    if (!deepEqual(updatedState, this.state())) {
+      this.state.set({
+        ...this.state(),
+        ...state,
+      });
+    }
+  }
+
+  private defaultPlots(): RatePlots {
+    return {
+      probability: null,
+      rate: null,
+    };
+  }
+}
diff --git a/projects/nshmp-apps/src/shared/services/shared.service.ts b/projects/nshmp-apps/src/shared/services/shared.service.ts
index bfd795a98de760cf6622bb8a2be212983524cfbf..effe8ddbc2d01f503f45c3b669675a86122c66a0 100644
--- a/projects/nshmp-apps/src/shared/services/shared.service.ts
+++ b/projects/nshmp-apps/src/shared/services/shared.service.ts
@@ -1,4 +1,9 @@
-import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
+import {
+  AbstractControl,
+  ValidationErrors,
+  ValidatorFn,
+  Validators,
+} from '@angular/forms';
 import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
 
 export interface ResetPlotSettingsProps {
@@ -28,4 +33,8 @@ export class SharedService {
       return isNaN(control.value) ? {nan: {value: control.value}} : null;
     };
   }
+
+  validateRequired(): ValidatorFn {
+    return (control: AbstractControl) => Validators.required(control);
+  }
 }