diff --git a/src/_zkapauthorizer/__init__.py b/src/_zkapauthorizer/__init__.py
index 5bfc4ba480c1699f0f936d27517cc40d8430dca8..b68f4ad55a090c8032082ca2986b8163b8a7098e 100644
--- a/src/_zkapauthorizer/__init__.py
+++ b/src/_zkapauthorizer/__init__.py
@@ -15,6 +15,11 @@
 __all__ = [
     "__version__",
     "__revision__",
+    "NAME",
 ]
 
+# The identifier for this plugin.  This appears in URLs for resources the
+# client plugin exposes, configuration files, etc.
+NAME = "privatestorageio-zkapauthz-v2"
+
 from ._version import __revision__, __version__
diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py
index 4076c7ca5a671762ffcd3f87228ec94965b2c097..f4bb2c21bfbbf1fcce7ca906538b1a802c677043 100644
--- a/src/_zkapauthorizer/api.py
+++ b/src/_zkapauthorizer/api.py
@@ -20,10 +20,6 @@ __all__ = [
     "ZKAPAuthorizer",
 ]
 
-# The identifier for this plugin.  This appears in URLs for resources the
-# client plugin exposes, configuration files, etc.
-NAME = "privatestorageio-zkapauthz-v2"
-
 from ._storage_client import ZKAPAuthorizerStorageClient
 from ._storage_server import LeaseRenewalRequired, ZKAPAuthorizerStorageServer
 from .storage_common import MorePassesRequired
diff --git a/src/_zkapauthorizer/config.py b/src/_zkapauthorizer/config.py
index f92cde0fa46a45dc490899a18edab5c70cf9fa2f..a3ab5882e7aef071d8117c28a4c609a068cf924c 100644
--- a/src/_zkapauthorizer/config.py
+++ b/src/_zkapauthorizer/config.py
@@ -21,7 +21,7 @@ from typing import Optional
 
 from allmydata.node import _Config
 
-from .api import NAME
+from . import NAME
 from .lease_maintenance import LeaseMaintenanceConfig
 
 
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index bccbf3d28d54d3a9cc01eb01e9b1ecea4b6180c6..dab10313767a4fef69d0b4da7dce332449a41f3f 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -290,20 +290,22 @@ class VoucherStore(object):
         if not isinstance(now, datetime):
             raise TypeError("{} returned {}, expected datetime".format(self.now, now))
 
+        voucher_text = voucher.decode("ascii")
+
         cursor.execute(
             """
             SELECT [text]
             FROM [tokens]
             WHERE [voucher] = ? AND [counter] = ?
             """,
-            (voucher.decode("ascii"), counter),
+            (voucher_text, counter),
         )
         rows = cursor.fetchall()
         if len(rows) > 0:
             self._log.info(
                 "Loaded {count} random tokens for a voucher ({voucher}[{counter}]).",
                 count=len(rows),
-                voucher=voucher,
+                voucher=voucher_text,
                 counter=counter,
             )
             tokens = list(
@@ -314,14 +316,14 @@ class VoucherStore(object):
             self._log.info(
                 "Persisting {count} random tokens for a voucher ({voucher}[{counter}]).",
                 count=len(tokens),
-                voucher=voucher.decode("ascii"),
+                voucher=voucher_text,
                 counter=counter,
             )
             cursor.execute(
                 """
                 INSERT OR IGNORE INTO [vouchers] ([number], [expected-tokens], [created]) VALUES (?, ?, ?)
                 """,
-                (voucher.decode("ascii"), expected_tokens, self.now()),
+                (voucher_text, expected_tokens, self.now()),
             )
             cursor.executemany(
                 """
@@ -329,7 +331,7 @@ class VoucherStore(object):
                 """,
                 list(
                     (
-                        voucher.decode("ascii"),
+                        voucher_text,
                         counter,
                         token.token_value.decode("ascii"),
                     )
@@ -392,11 +394,13 @@ class VoucherStore(object):
             token_count_increase = 0
             sequestered_count_increase = len(unblinded_tokens)
 
+        voucher_text = voucher.decode("ascii")
+
         cursor.execute(
             """
             INSERT INTO [redemption-groups] ([voucher], [public-key], [spendable]) VALUES (?, ?, ?)
             """,
-            (voucher.decode("ascii"), public_key, spendable),
+            (voucher_text, public_key, spendable),
         )
         group_id = cursor.lastrowid
 
@@ -422,7 +426,7 @@ class VoucherStore(object):
                 token_count_increase,
                 sequestered_count_increase,
                 self.now(),
-                voucher.decode("ascii"),
+                voucher_text,
             ),
         )
         if cursor.rowcount == 0:
@@ -430,6 +434,14 @@ class VoucherStore(object):
                 "Cannot insert tokens for unknown voucher; add voucher first"
             )
 
+        cursor.execute(
+            """
+            SELECT [counter] FROM [vouchers] WHERE [number] = ?
+            """,
+            (voucher_text,),
+        )
+        (new_counter,) = cursor.fetchone()
+
         cursor.executemany(
             """
             INSERT INTO [unblinded-tokens] ([token], [redemption-group]) VALUES (?, ?)
@@ -439,6 +451,19 @@ class VoucherStore(object):
                 for token in unblinded_tokens
             ),
         )
+        self._delete_corresponding_tokens(cursor, voucher_text, new_counter - 1)
+
+    def _delete_corresponding_tokens(self, cursor, voucher: str, counter: int) -> None:
+        """
+        Delete rows from the [tokens] table corresponding to the given redemption
+        group.
+        """
+        cursor.execute(
+            """
+            DELETE FROM [tokens] WHERE [voucher] = ? AND [counter] = ?
+            """,
+            (voucher, counter),
+        )
 
     @with_cursor
     def mark_voucher_double_spent(self, cursor, voucher):
@@ -496,7 +521,7 @@ class VoucherStore(object):
             of tokens available to be spent.  In this case, all tokens remain
             available to future calls and do not need to be reset.
 
-        :return list[UnblindedTokens]: The removed unblinded tokens.
+        :return list[UnblindedToken]: The removed unblinded tokens.
         """
         if count > _SQLITE3_INTEGER_MAX:
             # An unreasonable number of tokens and also large enough to
@@ -526,6 +551,17 @@ class VoucherStore(object):
         )
         return list(UnblindedToken(t.encode("ascii")) for (t,) in texts)
 
+    @with_cursor
+    def count_random_tokens(self, cursor) -> int:
+        """
+        :return: The number of random tokens present in the database.  This is
+        usually not interesting but it is exposed so the test suite can check
+        invariants related to it.
+        """
+        cursor.execute("SELECT count(1) FROM [tokens]")
+        (count,) = cursor.fetchone()
+        return count
+
     @with_cursor
     def count_unblinded_tokens(self, cursor):
         """
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index a06c862cb9bf0c8d3c9dd2183b6d44bbf83cdc20..d0edfb3d2ce0ff1e4e23bd55bae6edd77e86538a 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -29,10 +29,10 @@ from twisted.web.resource import ErrorPage, IResource, NoResource, Resource
 from twisted.web.server import NOT_DONE_YET
 from zope.interface import Attribute
 
+from . import NAME
 from . import __version__ as _zkapauthorizer_version
 from ._base64 import urlsafe_b64decode
 from ._json import dumps_utf8
-from .api import NAME
 from .config import get_configured_lease_duration
 from .controller import PaymentController, get_redeemer
 from .pricecalculator import PriceCalculator
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index c8e8786776d4414675848f400e294bdbe805c2ac..175039d8807fd5580a56ab05ca5d45d7b1c8ac38 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -22,7 +22,7 @@ from typing import Callable
 import attr
 from pyutil.mathutil import div_ceil
 
-from .api import NAME
+from . import NAME
 from .eliot import MUTABLE_PASSES_REQUIRED
 from .validators import greater_than
 
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 45d278012d75fcfcacdd5ec3976476baa6b7d6fe..2a03c8b17ff65f3484e7f13c80c14886b6a1bb15 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -46,7 +46,7 @@ from twisted.internet.task import Clock
 from twisted.web.test.requesthelper import DummyRequest
 from zope.interface import implementer
 
-from ..api import NAME
+from .. import NAME
 from ..configutil import config_string_from_sections
 from ..lease_maintenance import LeaseMaintenanceConfig, lease_maintenance_config_to_dict
 from ..model import (
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 61e741112e376d771107b90c17bdaf0ae7455e98..10f7bbcc1c8b9ef933473ad77d932fa6ff3b46a8 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -68,11 +68,11 @@ from twisted.web.http import BAD_REQUEST, NOT_FOUND, NOT_IMPLEMENTED, OK, UNAUTH
 from twisted.web.http_headers import Headers
 from twisted.web.resource import IResource, getChildForRequest
 
+from .. import NAME
 from .. import __file__ as package_init_file
 from .. import __version__ as zkapauthorizer_version
 from .._base64 import urlsafe_b64decode
 from .._json import dumps_utf8
-from ..api import NAME
 from ..configutil import config_string_from_sections
 from ..model import (
     DoubleSpend,
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 494e0af7940f1ecca5ae5fef42044a5069cfe1bf..7bd9aecb33ed096b1cc0bdf9b9707349caada4e6 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -452,6 +452,21 @@ class UnblindedTokenStateMachine(RuleBasedStateMachine):
         self.available += len(self.using)
         del self.using[:]
 
+    @invariant()
+    def random_token_count(self):
+        """
+        ``VoucherStore.random_token_count`` returns ``0``.
+
+        The state machine currently jumps over all intermediate states where
+        this function could legitimately return non-zero.  It might be nice to
+        split up the ``get_passes`` rule so that we could see other values
+        sometimes.
+        """
+        self.case.assertThat(
+            self.configless.store.count_random_tokens(),
+            Equals(0),
+        )
+
     @invariant()
     def unblinded_token_count(self):
         """
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index f1fc566d211f985b965583ea91ede11d9046e48b..1c50a993b3d5d9a485268ba8395dcb055c10c888 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -67,9 +67,9 @@ from twisted.web.resource import IResource
 
 from twisted.plugins.zkapauthorizer import storage_server
 
+from .. import NAME
 from .._plugin import get_root_nodes, load_signing_key
 from .._storage_client import IncorrectStorageServerReference
-from ..api import NAME
 from ..controller import DummyRedeemer, IssuerConfigurationMismatch, PaymentController
 from ..foolscap import RIPrivacyPassAuthorizedStorageServer
 from ..lease_maintenance import SERVICE_NAME, LeaseMaintenanceConfig
diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py
index 7b56840cbd5f175ff96debb3836dbe51fa5f1ef1..a793c37f7f7dd01c154a2b864010b87089652f04 100644
--- a/src/_zkapauthorizer/tests/test_storage_client.py
+++ b/src/_zkapauthorizer/tests/test_storage_client.py
@@ -36,9 +36,10 @@ from testtools.matchers import (
 from testtools.twistedsupport import failed, succeeded
 from twisted.internet.defer import fail, succeed
 
+from .. import NAME
 from .._storage_client import call_with_passes
 from .._storage_server import _ValidationResult
-from ..api import NAME, MorePassesRequired
+from ..api import MorePassesRequired
 from ..model import NotEnoughTokens
 from ..storage_common import (
     get_configured_allowed_public_keys,
diff --git a/src/twisted/plugins/zkapauthorizer.py b/src/twisted/plugins/zkapauthorizer.py
index ed116120a6cab7542701c2a8895e27a40d4deabc..041daaeb1f520a026b07777c2e2a786767a63f1d 100644
--- a/src/twisted/plugins/zkapauthorizer.py
+++ b/src/twisted/plugins/zkapauthorizer.py
@@ -16,6 +16,7 @@
 A drop-in to supply plugins to the Twisted plugin system.
 """
 
-from _zkapauthorizer.api import NAME, ZKAPAuthorizer
+from _zkapauthorizer import NAME
+from _zkapauthorizer.api import ZKAPAuthorizer
 
 storage_server = ZKAPAuthorizer(name=NAME)