diff --git a/default.nix b/default.nix
index 3a47363302f17d5aa412692d43ce90609fb6783d..04193ed1586856a9b21d7403a0cde175071b3344 100644
--- a/default.nix
+++ b/default.nix
@@ -1,2 +1,2 @@
-{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null }:
-pkgs.python27Packages.zkapauthorizer.override { inherit hypothesisProfile collectCoverage testSuite; }
+{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null, trialArgs ? [] }:
+pkgs.python27Packages.zkapauthorizer.override { inherit hypothesisProfile collectCoverage testSuite trialArgs; }
diff --git a/docs/source/interface.rst b/docs/source/interface.rst
index 261127a9e8f27f4f086f32e14b6a3a5a43197429..cabf775ffa7303759b286c003c14981a0c0f187d 100644
--- a/docs/source/interface.rst
+++ b/docs/source/interface.rst
@@ -48,3 +48,20 @@ The response is **OK** with ``application/json`` content-type response body like
   {"vouchers": [<voucher status object>, ...]}
 
 The elements of the list are objects like the one returned by issuing a **GET** to a child of this collection resource.
+
+``GET /storage-plugins/privatestorageio/zkapauthz-v1/unblinded-token``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This endpoint allows an external agent to retrieve unused unblinded tokens present in the node's database.
+Unblinded tokens are returned in ascending text sorted order.
+This endpoint accepts several query arguments:
+
+  * limit: An integer limiting the number of unblinded tokens to retrieve.
+  * position: A string which can be compared against unblinded token values.
+    Only unblinded tokens which sort as great than this value are returned.
+
+This endpoint accepts no request body.
+
+The response is **OK** with ``application/json`` content-type response body like::
+
+  {"total": <integer>, "unblinded-tokens": [<unblinded token string>, ...]}
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 3d78b9ddaae7491b0e00cf153855c7d4157d0498..56dae0b1da1551bbce5d2bf47101747f28e41d74 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -128,6 +128,10 @@ class NonRedeemer(object):
     """
     A ``NonRedeemer`` never tries to redeem vouchers for ZKAPs.
     """
+    @classmethod
+    def make(cls, section_name, node_config, announcement, reactor):
+        return cls()
+
     def random_tokens_for_voucher(self, voucher, count):
         # It doesn't matter because we're never going to try to redeem them.
         return list(
@@ -385,9 +389,11 @@ class PaymentController(object):
     store = attr.ib()
     redeemer = attr.ib()
 
-    def redeem(self, voucher):
+    def redeem(self, voucher, num_tokens=100):
         """
         :param unicode voucher: A voucher to redeem.
+
+        :param int num_tokens: A number of tokens to redeem.
         """
         # Pre-generate the random tokens to use when redeeming the voucher.
         # These are persisted with the voucher so the redemption can be made
@@ -399,7 +405,7 @@ class PaymentController(object):
         # number of passes that can be constructed is still only the size of
         # the set of random tokens.
         self._log.info("Generating random tokens for a voucher ({voucher}).", voucher=voucher)
-        tokens = self.redeemer.random_tokens_for_voucher(Voucher(voucher), 100)
+        tokens = self.redeemer.random_tokens_for_voucher(Voucher(voucher), num_tokens)
 
         # Persist the voucher and tokens so they're available if we fail.
         self._log.info("Persistenting random tokens for a voucher ({voucher}).", voucher=voucher)
@@ -413,6 +419,7 @@ class PaymentController(object):
             partial(self._redeemFailure, voucher),
         )
         d.addErrback(partial(self._finalRedeemError, voucher))
+        return d
 
     def _redeemSuccess(self, voucher, unblinded_tokens):
         """
@@ -443,6 +450,7 @@ def get_redeemer(plugin_name, node_config, announcement, reactor):
 
 
 _REDEEMERS = {
+    u"non": NonRedeemer.make,
     u"dummy": DummyRedeemer.make,
     u"ristretto": RistrettoRedeemer.make,
 }
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 76a659da860617fd26dc39afaebe3b25be8cad3a..5fa84f84de2dc533925d94298d06b8a131b3ce87 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -340,6 +340,22 @@ class VoucherStore(object):
             in texts
         )
 
+    @with_cursor
+    def backup(self, cursor):
+        """
+        Read out all state necessary to recreate this database in the event it is
+        lost.
+        """
+        cursor.execute(
+            """
+            SELECT [token] FROM [unblinded-tokens] ORDER BY [token]
+            """,
+        )
+        tokens = cursor.fetchall()
+        return {
+            u"unblinded-tokens": list(token for (token,) in tokens),
+        }
+
 
 @attr.s(frozen=True)
 class UnblindedToken(object):
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index c724fbca870dabb0457688c2ce162b91290161fd..9d13a02193ee85703e4e1dcb5b53f00c706c2dae 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -20,10 +20,18 @@ vouchers for fresh tokens.
 In the future it should also allow users to read statistics about token usage.
 """
 
+from sys import (
+    maxint,
+)
+from itertools import (
+    islice,
+)
 from json import (
     loads, dumps,
 )
-
+from zope.interface import (
+    Attribute,
+)
 from twisted.logger import (
     Logger,
 )
@@ -31,6 +39,7 @@ from twisted.web.http import (
     BAD_REQUEST,
 )
 from twisted.web.resource import (
+    IResource,
     ErrorPage,
     NoResource,
     Resource,
@@ -42,9 +51,17 @@ from ._base64 import (
 
 from .controller import (
     PaymentController,
-    NonRedeemer,
+    get_redeemer,
 )
 
+class IZKAPRoot(IResource):
+    """
+    The root of the resource tree of this plugin's client web presence.
+    """
+    store = Attribute("The ``VoucherStore`` used by this resource tree.")
+    controller = Attribute("The ``PaymentController`` used by this resource tree.")
+
+
 def from_configuration(node_config, store, redeemer=None):
     """
     Instantiate the plugin root resource using data from its configuration
@@ -63,13 +80,20 @@ def from_configuration(node_config, store, redeemer=None):
     :param IRedeemer redeemer: The voucher redeemer to use.  If ``None`` a
         sensible one is constructed.
 
-    :return IResource: The root of the resource hierarchy presented by the
+    :return IZKAPRoot: The root of the resource hierarchy presented by the
         client side of the plugin.
     """
     if redeemer is None:
-        redeemer = NonRedeemer()
+        redeemer = get_redeemer(
+            u"privatestorageio-zkapauthz-v1",
+            node_config,
+            None,
+            None,
+        )
     controller = PaymentController(store, redeemer)
     root = Resource()
+    root.store = store
+    root.controller = controller
     root.putChild(
         b"voucher",
         _VoucherCollection(
@@ -77,9 +101,63 @@ def from_configuration(node_config, store, redeemer=None):
             controller,
         ),
     )
+    root.putChild(
+        b"unblinded-token",
+        _UnblindedTokenCollection(
+            store,
+            controller,
+        ),
+    )
     return root
 
 
+def application_json(request):
+    """
+    Set the given request's response content-type to ``application/json``.
+
+    :param twisted.web.iweb.IRequest request: The request to modify.
+    """
+    request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"])
+
+
+class _UnblindedTokenCollection(Resource):
+    """
+    This class implements inspection of unblinded tokens.  Users **GET** this
+    resource to find out about unblinded tokens in the system.
+    """
+    _log = Logger()
+
+    def __init__(self, store, controller):
+        self._store = store
+        self._controller = controller
+        Resource.__init__(self)
+
+    def render_GET(self, request):
+        """
+        Retrieve some unblinded tokens and associated information.
+        """
+        application_json(request)
+        state = self._store.backup()
+        unblinded_tokens = state[u"unblinded-tokens"]
+
+        limit = request.args.get(b"limit", [None])[0]
+        if limit is not None:
+            limit = min(maxint, int(limit))
+
+        position = request.args.get(b"position", [b""])[0].decode("utf-8")
+
+        return dumps({
+            u"total": len(unblinded_tokens),
+            u"unblinded-tokens": list(islice((
+                token
+                for token
+                in unblinded_tokens
+                if token > position
+            ), limit)),
+        })
+
+
+
 class _VoucherCollection(Resource):
     """
     This class implements redemption of vouchers.  Users **PUT** such numbers
@@ -115,7 +193,7 @@ class _VoucherCollection(Resource):
 
 
     def render_GET(self, request):
-        request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"])
+        application_json(request)
         return dumps({
             u"vouchers": list(
                 voucher.marshal()
@@ -172,7 +250,7 @@ class VoucherView(Resource):
 
 
     def render_GET(self, request):
-        request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"])
+        application_json(request)
         return self._voucher.to_json()
 
 
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index dc9e0fb63df2a7872291f3fcdbf527662ae81617..c9c7ec226b7df5be6a4065850fa3fe07edd2280a 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -133,16 +133,6 @@ def minimal_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.
@@ -174,12 +164,41 @@ def server_configurations(signing_key_path):
     })
 
 
-def client_configurations():
+def client_ristrettoredeemer_configurations():
     """
-    Build configuration values for the client-side plugin.
+    Build Ristretto-using configuration values for the client-side plugin.
     """
     return just({
         u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
+        u"redeemer": u"ristretto",
+    })
+
+
+def client_dummyredeemer_configurations():
+    """
+    Build DummyRedeemer-using configuration values for the client-side plugin.
+    """
+    return just({
+        u"redeemer": u"dummy",
+    })
+
+
+def client_nonredeemer_configurations():
+    """
+    Build NonRedeemer-using configuration values for the client-side plugin.
+    """
+    return just({
+        u"redeemer": u"non",
+    })
+
+
+def tahoe_configs(zkapauthz_v1_configuration=client_dummyredeemer_configurations()):
+    """
+    Build complete Tahoe-LAFS configurations including the zkapauthorizer
+    client plugin section.
+    """
+    return minimal_tahoe_configs({
+        u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration,
     })
 
 
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 20047d3cf2b986112c386dd99e1cf782d87cb573..a73b7bb8a63b19bb3e7d0822692171ed0406cd47 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -44,9 +44,14 @@ from testtools import (
 from testtools.matchers import (
     MatchesStructure,
     MatchesAll,
+    AllMatch,
+    HasLength,
+    IsInstance,
+    ContainsDict,
     AfterPreprocessing,
     Equals,
     Always,
+    GreaterThan,
 )
 from testtools.twistedsupport import (
     CaptureTwistedLogs,
@@ -106,6 +111,8 @@ from ..resource import (
 
 from .strategies import (
     tahoe_configs,
+    client_dummyredeemer_configurations,
+    client_nonredeemer_configurations,
     vouchers,
     requests,
 )
@@ -205,30 +212,203 @@ def root_from_config(config):
     )
 
 
-class VoucherTests(TestCase):
+class ResourceTests(TestCase):
     """
-    Tests relating to ``/voucher`` as implemented by the
-    ``_zkapauthorizer.resource`` module and its handling of
-    vouchers.
+    General tests for the resources exposed by the plugin.
+    """
+    @given(tahoe_configs(), requests(just([u"unblinded-token"]) | just([u"voucher"])))
+    def test_reachable(self, get_config, request):
+        """
+        A resource is reachable at a child of the resource returned by
+        ``from_configuration``.
+        """
+        tempdir = self.useFixture(TempDir())
+        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        root = root_from_config(config)
+        self.assertThat(
+            getChildForRequest(root, request),
+            Provides([IResource]),
+        )
+
+
+class UnblindedTokenTests(TestCase):
+    """
+    Tests relating to ``/unblinded-token`` as implemented by the
+    ``_zkapauthorizer.resource`` module.
     """
     def setUp(self):
-        super(VoucherTests, self).setUp()
+        super(UnblindedTokenTests, self).setUp()
         self.useFixture(CaptureTwistedLogs())
 
 
-    @given(tahoe_configs(), requests(just([u"voucher"])))
-    def test_reachable(self, get_config, request):
+    @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100))
+    def test_get(self, get_config, voucher, num_tokens):
+        """
+        When the unblinded token collection receives a **GET**, the response is the
+        total number of unblinded tokens in the system and the unblinded tokens
+        themselves.
+        """
+        tempdir = self.useFixture(TempDir())
+        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        root = root_from_config(config)
+
+        if num_tokens:
+            # Put in a number of tokens with which to test.
+            redeeming = root.controller.redeem(voucher, num_tokens)
+            # Make sure the operation completed before proceeding.
+            self.assertThat(
+                redeeming,
+                succeeded(Always()),
+            )
+
+        agent = RequestTraversalAgent(root)
+        requesting = agent.request(
+            b"GET",
+            b"http://127.0.0.1/unblinded-token",
+        )
+        self.addDetail(
+            u"requesting result",
+            text_content(u"{}".format(vars(requesting.result))),
+        )
+        self.assertThat(
+            requesting,
+            succeeded_with_unblinded_tokens(num_tokens, num_tokens),
+        )
+
+    @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100), integers(min_value=0))
+    def test_get_limit(self, get_config, voucher, num_tokens, limit):
         """
-        A resource is reachable at the ``voucher`` child of a the resource
-        returned by ``from_configuration``.
+        When the unblinded token collection receives a **GET** with a **limit**
+        query argument, it returns no more unblinded tokens than indicated by
+        the limit.
         """
         tempdir = self.useFixture(TempDir())
         config = get_config(tempdir.join(b"tahoe"), b"tub.port")
         root = root_from_config(config)
+
+        if num_tokens:
+            # Put in a number of tokens with which to test.
+            redeeming = root.controller.redeem(voucher, num_tokens)
+            # Make sure the operation completed before proceeding.
+            self.assertThat(
+                redeeming,
+                succeeded(Always()),
+            )
+
+        agent = RequestTraversalAgent(root)
+        requesting = agent.request(
+            b"GET",
+            b"http://127.0.0.1/unblinded-token?limit={}".format(limit),
+        )
+        self.addDetail(
+            u"requesting result",
+            text_content(u"{}".format(vars(requesting.result))),
+        )
         self.assertThat(
-            getChildForRequest(root, request),
-            Provides([IResource]),
+            requesting,
+            succeeded_with_unblinded_tokens(num_tokens, min(num_tokens, limit)),
+        )
+
+    @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100), text(max_size=64))
+    def test_get_position(self, get_config, voucher, num_tokens, position):
+        """
+        When the unblinded token collection receives a **GET** with a **position**
+        query argument, it returns all unblinded tokens which sort greater
+        than the position and no others.
+        """
+        tempdir = self.useFixture(TempDir())
+        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        root = root_from_config(config)
+
+        if num_tokens:
+            # Put in a number of tokens with which to test.
+            redeeming = root.controller.redeem(voucher, num_tokens)
+            # Make sure the operation completed before proceeding.
+            self.assertThat(
+                redeeming,
+                succeeded(Always()),
+            )
+
+        agent = RequestTraversalAgent(root)
+        requesting = agent.request(
+            b"GET",
+            b"http://127.0.0.1/unblinded-token?position={}".format(
+                quote(position.encode("utf-8"), safe=b""),
+            ),
+        )
+        self.addDetail(
+            u"requesting result",
+            text_content(u"{}".format(vars(requesting.result))),
+        )
+        self.assertThat(
+            requesting,
+            succeeded_with_unblinded_tokens_with_matcher(
+                num_tokens,
+                AllMatch(
+                    MatchesAll(
+                        GreaterThan(position),
+                        IsInstance(unicode),
+                    ),
+                ),
+            ),
+        )
+
+
+def succeeded_with_unblinded_tokens_with_matcher(all_token_count, match_unblinded_tokens):
+    """
+    :return: A matcher which matches a Deferred which fires with a response
+        like the one returned by the **unblinded-tokens** endpoint.
+
+    :param int all_token_count: The expected value in the ``total`` field of
+        the response.
+
+    :param match_unblinded_tokens: A matcher for the ``unblinded-tokens``
+        field of the response.
+    """
+    return succeeded(
+        MatchesAll(
+            ok_response(headers=application_json()),
+            AfterPreprocessing(
+                json_content,
+                succeeded(
+                    ContainsDict({
+                        u"total": Equals(all_token_count),
+                        u"unblinded-tokens": match_unblinded_tokens,
+                    }),
+                ),
+            ),
+        ),
+    )
+
+def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
+    """
+    :return: A matcher which matches a Deferred which fires with a response
+        like the one returned by the **unblinded-tokens** endpoint.
+
+    :param int all_token_count: The expected value in the ``total`` field of
+        the response.
+
+    :param int returned_token_count: The expected number of tokens in the
+       ``unblinded-tokens`` field of the response.
+    """
+    return succeeded_with_unblinded_tokens_with_matcher(
+        all_token_count,
+        MatchesAll(
+            HasLength(returned_token_count),
+            AllMatch(IsInstance(unicode)),
         )
+    )
+
+
+class VoucherTests(TestCase):
+    """
+    Tests relating to ``/voucher`` as implemented by the
+    ``_zkapauthorizer.resource`` module and its handling of
+    vouchers.
+    """
+    def setUp(self):
+        super(VoucherTests, self).setUp()
+        self.useFixture(CaptureTwistedLogs())
 
 
     @given(tahoe_configs(), vouchers())
@@ -343,14 +523,32 @@ class VoucherTests(TestCase):
             ),
         )
 
+    @given(tahoe_configs(client_nonredeemer_configurations()), vouchers())
+    def test_get_known_voucher_unredeemed(self, get_config, voucher):
+        """
+        When a voucher is first ``PUT`` and then later a ``GET`` is issued for the
+        same voucher then the response code is **OK** and details about the
+        voucher are included in a json-encoded response body.
+        """
+        return self._test_get_known_voucher(get_config, voucher, False)
 
-    @given(tahoe_configs(), vouchers())
-    def test_get_known_voucher(self, get_config, voucher):
+    @given(tahoe_configs(client_dummyredeemer_configurations()), vouchers())
+    def test_get_known_voucher_redeemed(self, get_config, voucher):
         """
         When a voucher is first ``PUT`` and then later a ``GET`` is issued for the
         same voucher then the response code is **OK** and details about the
         voucher are included in a json-encoded response body.
         """
+        return self._test_get_known_voucher(get_config, voucher, True)
+
+    def _test_get_known_voucher(self, get_config, voucher, redeemed):
+        """
+        Assert that a voucher that is ``PUT`` and then ``GET`` is represented in
+        the JSON response.
+
+        :param bool redeemed: Whether the voucher is expected to be redeemed
+            or not in the response.
+        """
         tempdir = self.useFixture(TempDir())
         config = get_config(tempdir.join(b"tahoe"), b"tub.port")
         root = root_from_config(config)
@@ -390,7 +588,7 @@ class VoucherTests(TestCase):
                     AfterPreprocessing(
                         json_content,
                         succeeded(
-                            Equals(Voucher(voucher).marshal()),
+                            Equals(Voucher(voucher, redeemed=redeemed).marshal()),
                         ),
                     ),
                 ),
@@ -446,7 +644,7 @@ class VoucherTests(TestCase):
                         succeeded(
                             Equals({
                                 u"vouchers": list(
-                                    Voucher(voucher).marshal()
+                                    Voucher(voucher, redeemed=True).marshal()
                                     for voucher
                                     in vouchers
                                 ),
diff --git a/zkapauthorizer.nix b/zkapauthorizer.nix
index 462d8fbc23d692ccce570900ef4efc963e59c411..fb54924d419c80e94fcb45b4713d97f4fef5e4aa 100644
--- a/zkapauthorizer.nix
+++ b/zkapauthorizer.nix
@@ -4,10 +4,12 @@
 , hypothesisProfile ? null
 , collectCoverage ? false
 , testSuite ? null
+, trialArgs ? []
 }:
 let
   hypothesisProfile' = if hypothesisProfile == null then "default" else hypothesisProfile;
   testSuite' = if testSuite == null then "_zkapauthorizer" else testSuite;
+  extraTrialArgs = builtins.concatStringsSep " " trialArgs;
 in
 buildPythonPackage rec {
   version = "0.0";
@@ -39,15 +41,14 @@ buildPythonPackage rec {
     treq
   ];
 
-
-
   checkPhase = ''
     runHook preCheck
+    set -x
     "${pyflakes}/bin/pyflakes" src/_zkapauthorizer
     ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} python -m ${if collectCoverage
       then "coverage run --branch --source _zkapauthorizer,twisted.plugins.zkapauthorizer --module"
       else ""
-    } twisted.trial ${testSuite'}
+    } twisted.trial ${extraTrialArgs} ${testSuite'}
     runHook postCheck
   '';