diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 21e0189b2e899564daebf3a07e9e6c3d27e5bbe6..df38a6a8109fdccdb5658f3fca9f35ace576a8f6 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -73,6 +73,8 @@ from .model import (
     Error as model_Error,
 )
 
+# The number of tokens to submit with a voucher redemption.
+NUM_TOKENS = 100
 
 class AlreadySpent(Exception):
     """
@@ -513,34 +515,47 @@ class PaymentController(object):
     _unpaid = attr.ib(default=attr.Factory(dict))
     _active = attr.ib(default=attr.Factory(dict))
 
-    def redeem(self, voucher, num_tokens=100):
+    def __attrs_post_init__(self):
         """
-        :param unicode voucher: A voucher to redeem.
+        Check the voucher store for any vouchers in need of redemption.
 
-        :param int num_tokens: A number of tokens to redeem.
+        This is an initialization-time hook called by attrs.
         """
-        # Pre-generate the random tokens to use when redeeming the voucher.
-        # These are persisted with the voucher so the redemption can be made
-        # idempotent.  We don't want to lose the value if we fail after the
-        # server deems the voucher redeemed but before we persist the result.
-        # With a stable set of tokens, we can re-submit them and the server
-        # can re-sign them without fear of issuing excess passes.  Whether the
-        # server signs a given set of random tokens once or many times, the
-        # 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), num_tokens)
+        self._check_pending_vouchers()
 
-        # Persist the voucher and tokens so they're available if we fail.
-        self._log.info("Persistenting random tokens for a voucher ({voucher}).", voucher=voucher)
-        self.store.add(voucher, tokens)
+    def _check_pending_vouchers(self):
+        """
+        Find vouchers in the voucher store that need to be redeemed and try to
+        redeem them.
+        """
+        vouchers = self.store.list()
+        for voucher in vouchers:
+            if voucher.state.should_start_redemption():
+                self._log.info(
+                    "Controller found voucher ({}) at startup that needs redemption.",
+                    voucher=voucher.number,
+                )
+                random_tokens = self._get_random_tokens_for_voucher(voucher.number, NUM_TOKENS)
+                self._perform_redeem(voucher.number, random_tokens)
+            else:
+                self._log.info(
+                    "Controller found voucher ({}) at startup that does not need redemption.",
+                    voucher=voucher.number,
+                )
+
+    def _perform_redeem(self, voucher, random_tokens):
+        """
+        Use the redeemer to redeem the given voucher and random tokens.
 
+        This will not persist the voucher or random tokens but it will persist
+        the result.
+        """
         # Ask the redeemer to do the real task of redemption.
         self._log.info("Redeeming random tokens for a voucher ({voucher}).", voucher=voucher)
         d = bracket(
             lambda: setitem(self._active, voucher, self.store.now()),
             lambda: delitem(self._active, voucher),
-            lambda: self.redeemer.redeem(Voucher(voucher), tokens),
+            lambda: self.redeemer.redeem(Voucher(voucher), random_tokens),
         )
         d.addCallbacks(
             partial(self._redeemSuccess, voucher),
@@ -549,6 +564,43 @@ class PaymentController(object):
         d.addErrback(partial(self._finalRedeemError, voucher))
         return d
 
+    def _get_random_tokens_for_voucher(self, voucher, num_tokens):
+        """
+        Generate or load random tokens for a redemption attempt of a voucher.
+        """
+        self._log.info("Generating random tokens for a voucher ({voucher}).", voucher=voucher)
+        tokens = self.redeemer.random_tokens_for_voucher(Voucher(voucher), num_tokens)
+
+        self._log.info("Persistenting random tokens for a voucher ({voucher}).", voucher=voucher)
+        self.store.add(voucher, tokens)
+
+        # XXX If the voucher is already in the store then the tokens passed to
+        # `add` are dropped and we need to get the tokens already persisted in
+        # the store.  add should probably return them so we can use them.  Or
+        # maybe we should pass the generator in so it can be called only if
+        # necessary.
+        return tokens
+
+    def redeem(self, voucher, num_tokens=None):
+        """
+        :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
+        # idempotent.  We don't want to lose the value if we fail after the
+        # server deems the voucher redeemed but before we persist the result.
+        # With a stable set of tokens, we can re-submit them and the server
+        # can re-sign them without fear of issuing excess passes.  Whether the
+        # server signs a given set of random tokens once or many times, the
+        # number of passes that can be constructed is still only the size of
+        # the set of random tokens.
+        if num_tokens is None:
+            num_tokens = NUM_TOKENS
+        tokens = self._get_random_tokens_for_voucher(voucher, num_tokens)
+        return self._perform_redeem(voucher, tokens)
+
     def _redeemSuccess(self, voucher, unblinded_tokens):
         """
         Update the database state to reflect that a voucher was redeemed and to
@@ -587,6 +639,11 @@ class PaymentController(object):
         self._log.failure("Redeeming random tokens for a voucher ({voucher}) encountered error.", reason, voucher=voucher)
         return None
 
+    def get_voucher(self, number):
+        return self.incorporate_transient_state(
+            self.store.get(number),
+        )
+
     def incorporate_transient_state(self, voucher):
         """
         Create a new ``Voucher`` which represents the given voucher but which also
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 8af6d83234719a7525b802a3c2d00173a6f55c48..65d653ddf94ceaa3435fbaf9a2bb690029f4de4b 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -466,6 +466,9 @@ class RandomToken(object):
 
 @attr.s(frozen=True)
 class Pending(object):
+    def should_start_redemption(self):
+        return True
+
     def to_json_v1(self):
         return {
             u"name": u"pending",
@@ -481,6 +484,9 @@ class Redeeming(object):
     """
     started = attr.ib(validator=attr.validators.instance_of(datetime))
 
+    def should_start_redemption(self):
+        return False
+
     def to_json_v1(self):
         return {
             u"name": u"redeeming",
@@ -493,6 +499,9 @@ class Redeemed(object):
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
     token_count = attr.ib(validator=attr.validators.instance_of((int, long)))
 
+    def should_start_redemption(self):
+        return False
+
     def to_json_v1(self):
         return {
             u"name": u"redeemed",
@@ -505,6 +514,9 @@ class Redeemed(object):
 class DoubleSpend(object):
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
 
+    def should_start_redemption(self):
+        return False
+
     def to_json_v1(self):
         return {
             u"name": u"double-spend",
@@ -521,6 +533,9 @@ class Unpaid(object):
     """
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
 
+    def should_start_redemption(self):
+        return True
+
     def to_json_v1(self):
         return {
             u"name": u"unpaid",
@@ -538,6 +553,9 @@ class Error(object):
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
     details = attr.ib(validator=attr.validators.instance_of(unicode))
 
+    def should_start_redemption(self):
+        return True
+
     def to_json_v1(self):
         return {
             u"name": u"error",
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 93cab715a80861c96c78d5aa42bdbb43651a1029..1684e626808cb24acb5d0b2f5ca9629cb9071e0e 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -97,6 +97,7 @@ from ..controller import (
     NonRedeemer,
     DummyRedeemer,
     DoubleSpendRedeemer,
+    UnpaidRedeemer,
     RistrettoRedeemer,
     PaymentController,
     AlreadySpent,
@@ -110,6 +111,7 @@ from ..model import (
     Pending as model_Pending,
     DoubleSpend as model_DoubleSpend,
     Redeemed as model_Redeemed,
+    Unpaid as model_Unpaid,
 )
 
 from .strategies import (
@@ -212,6 +214,48 @@ class PaymentControllerTests(TestCase):
             ),
         )
 
+    @given(tahoe_configs(), datetimes(), vouchers())
+    def test_redeem_pending_on_startup(self, get_config, now, voucher):
+        """
+        When ``PaymentController`` is created, any vouchers in the store in the
+        pending state are redeemed.
+        """
+        tempdir = self.useFixture(TempDir())
+        store = VoucherStore.from_node_config(
+            get_config(
+                tempdir.join(b"node"),
+                b"tub.port",
+            ),
+            now=lambda: now,
+            connect=memory_connect,
+        )
+        # Create the voucher state in the store with a redemption that will
+        # certainly fail.
+        unpaid_controller = PaymentController(
+            store,
+            UnpaidRedeemer(),
+        )
+        unpaid_controller.redeem(voucher)
+
+        # Make sure we got where we wanted.
+        self.assertThat(
+            unpaid_controller.get_voucher(voucher).state,
+            IsInstance(model_Unpaid),
+        )
+
+        # Create another controller with the same store.  It will see the
+        # voucher state and attempt a redemption on its own.  It has I/O as an
+        # `__init__` side-effect. :/
+        success_controller = PaymentController(
+            store,
+            DummyRedeemer(),
+        )
+
+        self.assertThat(
+            success_controller.get_voucher(voucher).state,
+            IsInstance(model_Redeemed),
+        )
+
 
 NOWHERE = URL.from_text(u"https://127.0.0.1/")