diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..75bf5efc10320c156f5fa65de065de14e6d050f2
--- /dev/null
+++ b/docs/source/configuration.rst
@@ -0,0 +1,6 @@
+
+[storageclient.plugins.privatestorageio-zkapauthz-v1]
+redeemer = < dummy | ristretto >
+
+[storageserver.plugins.privatestorageio-zkapauthz-v1]
+ristretto-issuer-root-url = https://issuer.privatestorage.io/
diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index 1584a19dc169c70a2ed4a53f8b2d09aafa58ad53..b6e230c5d211b08c4882f9b01d3d4e15c11b4426 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -21,10 +21,6 @@ from weakref import (
     WeakValueDictionary,
 )
 
-from functools import (
-    partial,
-)
-
 import attr
 
 from zope.interface import (
@@ -54,7 +50,7 @@ from .resource import (
 )
 
 from .controller import (
-    DummyRedeemer,
+    get_redeemer,
 )
 
 @implementer(IAnnounceableStorageServer)
@@ -96,11 +92,24 @@ class ZKAPAuthorizer(object):
         return s
 
 
+    def _get_redeemer(self, node_config, announcement, reactor):
+        """
+        :return IRedeemer: The voucher redeemer indicated by the given
+            configuration.  A new instance is returned on every call because
+            the redeemer interface is stateless.
+        """
+        return get_redeemer(self.name, node_config, announcement, reactor)
+
+
     def get_storage_server(self, configuration, get_anonymous_storage_server):
-        announcement = {}
+        kwargs = configuration.copy()
+        root_url = kwargs.pop(u"ristretto-issuer-root-url")
+        announcement = {
+            u"ristretto-issuer-root-url": root_url,
+        }
         storage_server = ZKAPAuthorizerStorageServer(
             get_anonymous_storage_server(),
-            **configuration
+            **kwargs
         )
         return succeed(
             AnnounceableStorageServer(
@@ -117,16 +126,26 @@ class ZKAPAuthorizer(object):
         managed by this plugin in the node directory that goes along with
         ``node_config``.
         """
+        from twisted.internet import reactor
+        redeemer = self._get_redeemer(node_config, announcement, reactor)
+        extract_unblinded_tokens = self._get_store(node_config).extract_unblinded_tokens
+        # TODO: Make the caller figure out the correct number to extract.
+        def get_passes(message, count=1):
+            unblinded_tokens = extract_unblinded_tokens(count)
+            return redeemer.tokens_to_passes(message, unblinded_tokens)
+
         return ZKAPAuthorizerStorageClient(
             get_rref,
-            # TODO: Make the caller figure out the correct number to extract.
-            partial(self._get_store(node_config).extract_unblinded_tokens, 1),
+            get_passes,
         )
 
 
     def get_client_resource(self, node_config):
+        # XXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx........................ :(
+        announcement = {}
+        from twisted.internet import reactor
         return resource_from_configuration(
             node_config,
             store=self._get_store(node_config),
-            redeemer=DummyRedeemer(),
+            redeemer=self._get_redeemer(node_config, announcement, reactor),
         )
diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index e0dc41ddca2693016c17785a9dc4b655b7f2b585..55ccdb0c5db4d30e6977a90fad7f1f4de08d7a22 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -49,8 +49,11 @@ class ZKAPAuthorizerStorageClient(object):
         valid ``RemoteReference`` corresponding to the server-side object for
         this scheme.
 
-    :ivar _get_passes: A no-argument callable which retrieves some passes
-        which can be used to authorize an operation.
+    :ivar _get_passes: A two-argument callable which retrieves some passes
+        which can be used to authorize an operation.  The first argument is a
+        bytes message binding the passes to the request for which they will be
+        used.  The second is an integer giving the number of passes to
+        request.
     """
     _get_rref = attr.ib()
     _get_passes = attr.ib()
@@ -59,7 +62,7 @@ class ZKAPAuthorizerStorageClient(object):
     def _rref(self):
         return self._get_rref()
 
-    def _get_encoded_passes(self):
+    def _get_encoded_passes(self, message, count):
         """
         :return: A list of passes from ``_get_passes`` encoded into their
             ``bytes`` representation.
@@ -67,7 +70,7 @@ class ZKAPAuthorizerStorageClient(object):
         return list(
             t.text.encode("ascii")
             for t
-            in self._get_passes()
+            in self._get_passes(message, count)
         )
 
     def get_version(self):
@@ -86,7 +89,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "allocate_buckets",
-            self._get_encoded_passes(),
+            self._get_encoded_passes(storage_index, 1),
             storage_index,
             renew_secret,
             cancel_secret,
@@ -112,7 +115,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "add_lease",
-            self._get_encoded_passes(),
+            self._get_encoded_passes(storage_index, 1),
             storage_index,
             renew_secret,
             cancel_secret,
@@ -125,7 +128,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "renew_lease",
-            self._get_encoded_passes(),
+            self._get_encoded_passes(storage_index, 1),
             storage_index,
             renew_secret,
         )
@@ -154,7 +157,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "slot_testv_and_readv_and_writev",
-            self._get_encoded_passes(),
+            self._get_encoded_passes(storage_index, 1),
             storage_index,
             secrets,
             tw_vectors,
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index f415824d732f77c09b4afd7fc26d2b348053adb4..c2a890ca8c68a630d267cec01113423a1c448c80 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -39,9 +39,15 @@ from twisted.internet.defer import (
     inlineCallbacks,
     returnValue,
 )
+from twisted.web.client import (
+    Agent,
+)
 from treq import (
     json_content,
 )
+from treq.client import (
+    HTTPClient,
+)
 
 import privacypass
 
@@ -49,6 +55,7 @@ from .model import (
     RandomToken,
     UnblindedToken,
     Voucher,
+    Pass,
 )
 
 
@@ -131,6 +138,10 @@ class DummyRedeemer(object):
     really redeeming them, it makes up some fake ZKAPs and pretends those are
     the result.
     """
+    @classmethod
+    def make(cls, section_name, node_config, announcement, reactor):
+        return cls()
+
     def random_tokens_for_voucher(self, voucher, count):
         """
         Generate some number of random tokens to submit along with a voucher for
@@ -156,6 +167,13 @@ class DummyRedeemer(object):
             ),
         )
 
+    def tokens_to_passes(self, message, unblinded_tokens):
+        return list(
+            Pass(token.text)
+            for token
+            in unblinded_tokens
+        )
+
 
 @implementer(IRedeemer)
 @attr.s
@@ -163,6 +181,13 @@ class RistrettoRedeemer(object):
     _treq = attr.ib()
     _api_root = attr.ib(validator=attr.validators.instance_of(URL))
 
+    @classmethod
+    def make(cls, section_name, node_config, announcement, reactor):
+        return cls(
+            HTTPClient(Agent(reactor)),
+            URL.from_text(announcement[u"ristretto-issuer-root-url"]),
+        )
+
     def random_tokens_for_voucher(self, voucher, count):
         return list(
             RandomToken(privacypass.RandomToken.create().encode_base64().decode("ascii"))
@@ -220,8 +245,9 @@ class RistrettoRedeemer(object):
         ))
 
     def tokens_to_passes(self, message, unblinded_tokens):
-        # XXX Here's some more of the privacypass dance.  Something needs to
-        # know to call this, I guess?  Also it's untested as heck.
+        assert isinstance(message, bytes)
+        assert isinstance(unblinded_tokens, list)
+        assert all(isinstance(element, UnblindedToken) for element in unblinded_tokens)
         unblinded_tokens = list(
             privacypass.UnblindedToken.decode_base64(token.text.encode("ascii"))
             for token
@@ -252,7 +278,11 @@ class RistrettoRedeemer(object):
             for (token_preimage, sig)
             in clients_passes
         )
-        return marshaled_passes
+        return list(
+            Pass(p)
+            for p
+            in marshaled_passes
+        )
 
 
 @attr.s
@@ -309,3 +339,19 @@ class PaymentController(object):
         passes later).
         """
         self.store.insert_unblinded_tokens_for_voucher(voucher, unblinded_tokens)
+
+
+def get_redeemer(plugin_name, node_config, announcement, reactor):
+    section_name = u"storageclient.plugins.{}".format(plugin_name)
+    redeemer_kind = node_config.get_config(
+        section=section_name,
+        option=u"redeemer",
+        default=u"ristretto",
+    )
+    return _REDEEMERS[redeemer_kind](section_name, node_config, announcement, reactor)
+
+
+_REDEEMERS = {
+    u"dummy": DummyRedeemer.make,
+    u"ristretto": RistrettoRedeemer.make,
+}
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index f35371cc1a4f904ddaeed4bfc71293a3930e8688..a57170df008a055d7f71256e80343f707372ebca 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -119,7 +119,7 @@ def tahoe_config_texts(storage_client_plugins):
     )
 
 
-def tahoe_configs(storage_client_plugins=None):
+def minimal_tahoe_configs(storage_client_plugins=None):
     """
     Build complete Tahoe-LAFS configurations for a node.
     """
@@ -135,6 +135,17 @@ def tahoe_configs(storage_client_plugins=None):
         ),
     )
 
+
+def tahoe_configs():
+    """
+    Build complete Tahoe-LAFS configurations including the zkapauthorizer
+    client plugin section.
+    """
+    return minimal_tahoe_configs({
+        u"privatestorageio-zkapauthz-v1": client_configurations(),
+    })
+
+
 def node_nicknames():
     """
     Builds Tahoe-LAFS node nicknames.
@@ -153,11 +164,13 @@ def node_nicknames():
     )
 
 
-def configurations():
+def server_configurations():
     """
     Build configuration values for the server-side plugin.
     """
-    return just({})
+    return just({
+        u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
+    })
 
 
 def client_configurations():
@@ -435,4 +448,6 @@ def announcements():
     """
     Build announcements for the ZKAPAuthorizer plugin.
     """
-    return just({})
+    return just({
+        u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
+    })
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 203e44ca3ca557788daefd667fbbea3dc73de4e5..77b5bb38228a73044f7c08f98d460a024138c8b2 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -102,7 +102,6 @@ from ..resource import (
 
 from .strategies import (
     tahoe_configs,
-    client_configurations,
     vouchers,
     requests,
 )
@@ -121,10 +120,6 @@ def uncooperator(started=True):
 
 
 
-tahoe_configs_with_client_config = tahoe_configs(storage_client_plugins={
-    u"privatestorageio-zkapauthz-v1": client_configurations(),
-})
-
 def is_not_json(bytestring):
     """
     :param bytes bytestring: A candidate byte string to inspect.
@@ -217,7 +212,7 @@ class VoucherTests(TestCase):
         self.useFixture(CaptureTwistedLogs())
 
 
-    @given(tahoe_configs_with_client_config, requests(just([u"voucher"])))
+    @given(tahoe_configs(), requests(just([u"voucher"])))
     def test_reachable(self, get_config, request):
         """
         A resource is reachable at the ``voucher`` child of a the resource
@@ -232,7 +227,7 @@ class VoucherTests(TestCase):
         )
 
 
-    @given(tahoe_configs_with_client_config, vouchers())
+    @given(tahoe_configs(), vouchers())
     def test_put_voucher(self, get_config, voucher):
         """
         When a voucher is ``PUT`` to ``VoucherCollection`` it is passed in to the
@@ -263,7 +258,7 @@ class VoucherTests(TestCase):
             ),
         )
 
-    @given(tahoe_configs_with_client_config, invalid_bodies())
+    @given(tahoe_configs(), invalid_bodies())
     def test_put_invalid_body(self, get_config, body):
         """
         If the body of a ``PUT`` to ``VoucherCollection`` does not consist of an
@@ -294,7 +289,7 @@ class VoucherTests(TestCase):
             ),
         )
 
-    @given(tahoe_configs_with_client_config, not_vouchers())
+    @given(tahoe_configs(), not_vouchers())
     def test_get_invalid_voucher(self, get_config, not_voucher):
         """
         When a syntactically invalid voucher is requested with a ``GET`` to a
@@ -322,7 +317,7 @@ class VoucherTests(TestCase):
         )
 
 
-    @given(tahoe_configs_with_client_config, vouchers())
+    @given(tahoe_configs(), vouchers())
     def test_get_unknown_voucher(self, get_config, voucher):
         """
         When a voucher is requested with a ``GET`` to a child of
@@ -345,7 +340,7 @@ class VoucherTests(TestCase):
         )
 
 
-    @given(tahoe_configs_with_client_config, vouchers())
+    @given(tahoe_configs(), vouchers())
     def test_get_known_voucher(self, get_config, voucher):
         """
         When a voucher is first ``PUT`` and then later a ``GET`` is issued for the
@@ -398,7 +393,7 @@ class VoucherTests(TestCase):
             ),
         )
 
-    @given(tahoe_configs_with_client_config, lists(vouchers(), unique=True))
+    @given(tahoe_configs(), lists(vouchers(), unique=True))
     def test_list_vouchers(self, get_config, vouchers):
         """
         A ``GET`` to the ``VoucherCollection`` itself returns a list of existing
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index 8c0f648d77f7bf80c5fab8e5fe72d498faf9a1dd..b37cdbf6692dadb2657db85a4469703c4642cce5 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -36,11 +36,12 @@ from testtools.matchers import (
 from testtools.twistedsupport import (
     succeeded,
 )
-
 from hypothesis import (
     given,
 )
-
+from hypothesis.strategies import (
+    just,
+)
 from foolscap.broker import (
     Broker,
 )
@@ -77,8 +78,9 @@ from ..model import (
 )
 
 from .strategies import (
+    minimal_tahoe_configs,
     tahoe_configs,
-    configurations,
+    server_configurations,
     announcements,
     vouchers,
     random_tokens,
@@ -134,7 +136,7 @@ class ServerPluginTests(TestCase):
     Tests for the plugin's implementation of
     ``IFoolscapStoragePlugin.get_storage_server``.
     """
-    @given(configurations())
+    @given(server_configurations())
     def test_returns_announceable(self, configuration):
         """
         ``storage_server.get_storage_server`` returns an instance which provides
@@ -150,7 +152,7 @@ class ServerPluginTests(TestCase):
         )
 
 
-    @given(configurations())
+    @given(server_configurations())
     def test_returns_referenceable(self, configuration):
         """
         The storage server attached to the result of
@@ -171,7 +173,7 @@ class ServerPluginTests(TestCase):
             ),
         )
 
-    @given(configurations())
+    @given(server_configurations())
     def test_returns_serializable(self, configuration):
         """
         The storage server attached to the result of
@@ -195,7 +197,7 @@ class ServerPluginTests(TestCase):
         )
 
 
-    @given(configurations())
+    @given(server_configurations())
     def test_returns_hashable(self, configuration):
         """
         The storage server attached to the result of
@@ -221,6 +223,10 @@ class ServerPluginTests(TestCase):
         )
 
 
+tahoe_configs_with_dummy_redeemer = minimal_tahoe_configs({
+    u"privatestorageio-zkapauthz-v1": just({u"redeemer": u"dummy"}),
+})
+
 
 class ClientPluginTests(TestCase):
     """
@@ -252,7 +258,7 @@ class ClientPluginTests(TestCase):
 
 
     @given(
-        tahoe_configs(),
+        tahoe_configs_with_dummy_redeemer,
         announcements(),
         vouchers(),
         random_tokens(),
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 51cac393c395cdd3dc520aacc864dc81028e6d3b..73f9017efa02cc256b05dc224ee6ef461b5488b7 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -160,8 +160,8 @@ class ShareTests(TestCase):
         self.canary = LocalReferenceable(None)
         self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server
 
-        def get_passes():
-            return [Pass(u"x" * TOKEN_LENGTH)]
+        def get_passes(message, count):
+            return [Pass(u"x" * TOKEN_LENGTH)] * count
 
         self.server = ZKAPAuthorizerStorageServer(
             self.anonymous_storage_server,
@@ -512,8 +512,8 @@ class ShareTests(TestCase):
         # transit the network differently from keyword arguments.  Yay.
         d = self.client._rref.callRemote(
             "slot_testv_and_readv_and_writev",
-            # tokens
-            self.client._get_encoded_passes(),
+            # passes
+            self.client._get_encoded_passes(storage_index, 1),
             # storage_index
             storage_index,
             # secrets