diff --git a/nixos/modules/spending.nix b/nixos/modules/spending.nix
new file mode 100644
index 0000000000000000000000000000000000000000..32470b4689725f64d1e341ebdaa1d5a59b34c74b
--- /dev/null
+++ b/nixos/modules/spending.nix
@@ -0,0 +1,87 @@
+# A NixOS module which can run a Ristretto-based issuer for PrivateStorage
+# ZKAPs.
+{ lib, pkgs, config, ourpkgs, ... }@args: let
+  cfg = config.services.private-storage-spending;
+in
+{
+  options = {
+    services.private-storage-spending = {
+      enable = lib.mkEnableOption "PrivateStorage Spending Service";
+      package = lib.mkOption {
+        default = ourpkgs.zkap-spending-service;
+        type = lib.types.package;
+        example = lib.literalExample "ourpkgs.zkap-spending-service";
+        description = ''
+          The package to use for the spending service.
+        '';
+      };
+      unixSocket = lib.mkOption {
+        default = "/run/zkap-spending-service/api.socket";
+        type = lib.types.path;
+        description = ''
+          The unix socket that the spending service API listens on.
+        '';
+      };
+    };
+    services.private-storage-spending.domain = lib.mkOption {
+      default = config.networking.fqdn;
+      type = lib.types.str;
+      example = lib.literalExample [ "spending.example.com" ];
+      description = ''
+        The domain name at which the spending service is reachable.
+      '';
+    };
+  };
+
+  config =
+    lib.mkIf cfg.enable {
+      systemd.sockets.zkap-spending-service = {
+        enable = true;
+        wantedBy = [ "sockets.target" ];
+        listenStreams = [ cfg.unixSocket ];
+      };
+      # Add a systemd service to run zkap-spending-service.
+      systemd.services.zkap-spending-service = {
+        enable = true;
+        description = "ZKAP Spending Service";
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig.NonBlocking = true;
+
+        # It really shouldn't ever exit on its own!  If it does, it's a bug
+        # we'll have to fix.  Restart it and hope it doesn't happen too much
+        # before we can fix whatever the issue is.
+        serviceConfig.Restart = "always";
+        serviceConfig.Type = "simple";
+
+        script = let
+          httpArgs = "--http-endpoint systemd:domain=UNIX:index=0";
+        in
+          "exec ${cfg.package}/bin/${cfg.package.meta.mainProgram} run ${httpArgs}";
+      };
+
+      services.nginx = {
+        enable = true;
+
+        recommendedGzipSettings = true;
+        recommendedOptimisation = true;
+        recommendedProxySettings = true;
+        recommendedTlsSettings = true;
+
+        virtualHosts."${cfg.domain}" = {
+          locations."/v1/" = {
+            # Only forward requests beginning with /v1/ so
+            # we pass less scanning spam on to our backend
+            # Want a regex instead? try locations."~ /v\d+/"
+            proxyPass = "http://unix:${cfg.unixSocket}";
+          };
+          locations."/" = {
+            # Return a 404 error for any paths not specified above.
+            extraConfig = ''
+              return 404;
+            '';
+          };
+        };
+      };
+    };
+}
diff --git a/nixos/system-tests.nix b/nixos/system-tests.nix
index 73b6665ab91e4d9a8a2200fb0eec7ff596f79b39..7b6d382ada53c1121a1bc3d0edbf82964d644ad2 100644
--- a/nixos/system-tests.nix
+++ b/nixos/system-tests.nix
@@ -3,5 +3,6 @@ let
   pkgs = import ../nixpkgs-2105.nix { };
 in {
   private-storage = pkgs.nixosTest ./tests/private-storage.nix;
+  spending = pkgs.nixosTest ./tests/spending.nix;
   tahoe = pkgs.nixosTest ./tests/tahoe.nix;
 }
diff --git a/nixos/tests/spending.nix b/nixos/tests/spending.nix
new file mode 100644
index 0000000000000000000000000000000000000000..c970157b9375e0d99e2be8d4f782992163a6c948
--- /dev/null
+++ b/nixos/tests/spending.nix
@@ -0,0 +1,32 @@
+{ pkgs, lib, ... }:
+{
+  name = "zkap-spending-service";
+  nodes = {
+    spending = { config, pkgs, ourpkgs, modulesPath, ... }: {
+      imports = [
+        ../modules/packages.nix
+        ../modules/spending.nix
+      ];
+
+      services.private-storage-spending.enable = true;
+      services.private-storage-spending.domain = "localhost";
+    };
+  };
+  testScript = { nodes }: let
+    revision = nodes.spending.config.passthru.ourpkgs.zkap-spending-service.meta.rev;
+    curl = "${pkgs.curl}/bin/curl -sSf";
+  in
+    ''
+      import json
+
+      start_all()
+
+      spending.wait_for_open_port(80)
+      with subtest("Ensure we can ping the spending service"):
+        output = spending.succeed("${curl} http://localhost/v1/_ping")
+        assert json.loads(output)["status"] == "ok", "Could not ping spending service."
+      with subtest("Ensure that the spending service version matches the expected version"):
+        output = spending.succeed("${curl} http://localhost/v1/_version")
+        assert json.loads(output)["revision"] == "${revision}", "Spending service revision does not match."
+    '';
+}