diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index 4e19a3973db20994f044f5cf6b199a87479a6723..e65395564042e697efeb392904d9a931cc46a510 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -64,10 +64,6 @@ from .api import (
     ZKAPAuthorizerStorageClient,
 )
 
-from .eliot import (
-    GET_PASSES,
-)
-
 from .model import (
     VoucherStore,
 )
@@ -82,6 +78,10 @@ from .storage_common import (
 from .controller import (
     get_redeemer,
 )
+from .spending import (
+    SpendingController,
+)
+
 from .lease_maintenance import (
     SERVICE_NAME,
     lease_maintenance_service,
@@ -173,16 +173,13 @@ class ZKAPAuthorizer(object):
         """
         from twisted.internet import reactor
         redeemer = self._get_redeemer(node_config, announcement, reactor)
-        extract_unblinded_tokens = self._get_store(node_config).extract_unblinded_tokens
-        def get_passes(message, count):
-            unblinded_tokens = extract_unblinded_tokens(count)
-            passes = redeemer.tokens_to_passes(message, unblinded_tokens)
-            GET_PASSES.log(
-                message=message,
-                count=count,
-            )
-            return passes
-
+        store = self._get_store(node_config)
+        # XXX Need to ensure one of these per store
+        controller = SpendingController(
+            store.extract_unblinded_tokens,
+            redeemer.tokens_to_passes,
+        )
+        get_passes = controller.get
         return ZKAPAuthorizerStorageClient(
             get_configured_pass_value(node_config),
             get_rref,
diff --git a/src/_zkapauthorizer/spending.py b/src/_zkapauthorizer/spending.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac0353c6595cfa002c27c4ef0432df44cc8bc63f
--- /dev/null
+++ b/src/_zkapauthorizer/spending.py
@@ -0,0 +1,42 @@
+# Copyright 2019 PrivateStorage.io, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+A module for logic controlling the manner in which ZKAPs are spent.
+"""
+
+import attr
+
+from .eliot import (
+    GET_PASSES,
+)
+
+
+@attr.s
+class SpendingController(object):
+    """
+    A ``SpendingController`` gives out ZKAPs and arranges for re-spend
+    attempts when necessary.
+    """
+    extract_unblinded_tokens = attr.ib()
+    tokens_to_passes = attr.ib()
+
+    def get(self, message, num_passes):
+        unblinded_tokens = self.extract_unblinded_tokens(num_passes)
+        passes = self.tokens_to_passes(message, unblinded_tokens)
+        GET_PASSES.log(
+            message=message,
+            count=num_passes,
+        )
+        return passes
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index ebd714863a3ab95a590826698001ba4cac469965..da72930e2a9e497beed3a0f4e143a36603e896a6 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -104,7 +104,7 @@ from twisted.plugins.zkapauthorizer import (
     storage_server,
 )
 
-from .._plugin import (
+from ..spending import (
     GET_PASSES,
 )