diff --git a/setup.cfg b/setup.cfg
index dfc4960747ba2d57bea9b12deff72f59371cd743..42dd1649cf7c1b05292f9e6f17647bec69143f59 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,6 +42,7 @@ install_requires =
     Twisted[tls,conch]>=18.4.0
     tahoe-lafs==1.14.0
     treq
+    pyutil
 
 [versioneer]
 VCS = git
diff --git a/src/_zkapauthorizer/pricecalculator.py b/src/_zkapauthorizer/pricecalculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..007ec9cdf212839ddabbac9555308b29fb158483
--- /dev/null
+++ b/src/_zkapauthorizer/pricecalculator.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 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.
+
+"""
+Calculate the price, in ZKAPs, for storing files.
+
+The underlying storage system operates only on individual shares.  Thus, it
+*does not* use this file-oriented calculator.  However, for end-users,
+file-oriented pricing is often more helpful.  This calculator builds on the
+share-oriented price calculation to present file-oriented price information.
+
+It accounts for erasure encoding data expansion.  It does not account for the
+real state of the storage system (e.g., if some data is *already* stored then
+storing it "again" is essentially free but this will not be reflected by this
+calculator).
+"""
+
+import attr
+
+from .storage_common import (
+    required_passes,
+    share_size_for_data,
+)
+
+@attr.s
+class PriceCalculator(object):
+    """
+    :ivar int _shares_needed: The number of shares which are required to
+        reconstruct the original data.
+
+    :ivar int _shares_total: The total number of shares which will be
+        produced in the erasure encoding process.
+
+    :ivar int _pass_value: The bytes component of the bytes×time value of a
+        single pass.
+    """
+    _shares_needed = attr.ib()
+    _shares_total = attr.ib()
+    _pass_value = attr.ib()
+
+    def calculate(self, sizes):
+        """
+        Calculate the price to store data of the given sizes for one lease
+        period.
+
+        :param [int] sizes: The sizes of the individual data items in bytes.
+
+        :return int: The number of ZKAPs required.
+        """
+        share_sizes = (
+            share_size_for_data(self._shares_needed, size)
+            for size
+            in sizes
+        )
+        all_required_passes = (
+            required_passes(self._pass_value, [share_size] * self._shares_total)
+            for share_size
+            in share_sizes
+        )
+        price = sum(all_required_passes, 0)
+        return price
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 20f1519d0196268b1886b0b3e9938c807886dbac..fae91cd2aee22e7809e2472dce909658bfb17eb5 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -61,11 +61,16 @@ from ._base64 import (
 )
 
 from .storage_common import (
-    required_passes,
+    get_configured_shares_needed,
+    get_configured_shares_total,
     get_configured_pass_value,
     get_configured_lease_duration,
 )
 
+from .pricecalculator import (
+    PriceCalculator,
+)
+
 from .controller import (
     PaymentController,
     get_redeemer,
@@ -117,13 +122,23 @@ def from_configuration(node_config, store, redeemer=None, default_token_count=No
     if default_token_count is None:
         default_token_count = NUM_TOKENS
     controller = PaymentController(store, redeemer, default_token_count)
+
+    calculator = PriceCalculator(
+        get_configured_shares_needed(node_config),
+        get_configured_shares_total(node_config),
+        get_configured_pass_value(node_config),
+    )
+    calculate_price = _CalculatePrice(
+        calculator,
+        get_configured_lease_duration(node_config),
+    )
+
     root = create_private_tree(
         lambda: node_config.get_private_config(b"api_auth_token"),
         authorizationless_resource_tree(
             store,
             controller,
-            get_configured_pass_value(node_config),
-            get_configured_lease_duration(node_config),
+            calculate_price,
         ),
     )
     root.store = store
@@ -131,15 +146,19 @@ def from_configuration(node_config, store, redeemer=None, default_token_count=No
     return root
 
 
-def authorizationless_resource_tree(store, controller, pass_value, lease_duration):
+def authorizationless_resource_tree(
+        store,
+        controller,
+        calculate_price,
+):
     """
     Create the full ZKAPAuthorizer client plugin resource hierarchy with no
     authorization applied.
 
     :param VoucherStore store: The store to use.
     :param PaymentController controller: The payment controller to use.
-    :param int pass_value: The bytes component of the bytes×time value of a single pass.
-    :param int lease_duration: The number of seconds a lease will be valid.
+
+    :param IResource calculate_price: The resource for the price calculation endpoint.
 
     :return IResource: The root of the resource hierarchy.
     """
@@ -164,7 +183,7 @@ def authorizationless_resource_tree(store, controller, pass_value, lease_duratio
     )
     root.putChild(
         b"calculate-price",
-        _CalculatePrice(pass_value, lease_duration),
+        calculate_price,
     )
     return root
 
@@ -177,12 +196,14 @@ class _CalculatePrice(Resource):
 
     render_HEAD = render_GET = None
 
-    def __init__(self, pass_value, lease_period):
+    def __init__(self, price_calculator, lease_period):
         """
-        :param pass_value: See ``authorizationless_resource_tree``
+        :param _PriceCalculator price_calculator: The object which can actually
+            calculate storage prices.
+
         :param lease_period: See ``authorizationless_resource_tree``
         """
-        self._pass_value = pass_value
+        self._price_calculator = price_calculator
         self._lease_period = lease_period
         Resource.__init__(self)
 
@@ -219,15 +240,22 @@ class _CalculatePrice(Resource):
                 "error": "did not find required version number 1 in request",
             })
 
-        if not isinstance(sizes, list) or not all(isinstance(size, (int, long)) and size >= 0 for size in sizes):
+        if (not isinstance(sizes, list) or
+            not all(
+                isinstance(size, (int, long)) and size >= 0
+                for size
+                in sizes
+        )):
             request.setResponseCode(BAD_REQUEST)
             return dumps({
                 "error": "did not find required positive integer sizes list in request",
             })
 
         application_json(request)
+
+        price = self._price_calculator.calculate(sizes)
         return dumps({
-            u"price": required_passes(self._pass_value, sizes),
+            u"price": price,
             u"period": self._lease_period,
         })
 
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index 38a46d50b3afd253857c054abf38fe212ef37cd6..dd9a9f49e15bd542aa70cbbbdaadedfde5c64bbb 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -34,6 +34,10 @@ from .eliot import (
     MUTABLE_PASSES_REQUIRED,
 )
 
+from pyutil.mathutil import (
+    div_ceil,
+)
+
 @attr.s(frozen=True)
 class MorePassesRequired(Exception):
     """
@@ -73,6 +77,34 @@ slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_
 # submitted.
 BYTES_PER_PASS = 1024 * 1024
 
+def get_configured_shares_needed(node_config):
+    """
+    Determine the configured-specified value of "needed" shares (``k``).
+
+    If no value is explicitly configured, the Tahoe-LAFS default (as best as
+    we know it) is returned.
+    """
+    return int(node_config.get_config(
+        section=u"client",
+        option=u"shares.needed",
+        default=3,
+    ))
+
+
+def get_configured_shares_total(node_config):
+    """
+    Determine the configured-specified value of "total" shares (``N``).
+
+    If no value is explicitly configured, the Tahoe-LAFS default (as best as
+    we know it) is returned.
+    """
+    return int(node_config.get_config(
+        section=u"client",
+        option=u"shares.total",
+        default=10,
+    ))
+
+
 def get_configured_pass_value(node_config):
     """
     Determine the configuration-specified value of a single ZKAP.
@@ -126,6 +158,23 @@ def required_passes(bytes_per_pass, share_sizes):
     return result
 
 
+def share_size_for_data(shares_needed, datasize):
+    """
+    Calculate the size of a single erasure encoding share for data of the
+    given size and with the given level of redundancy.
+
+    :param int shares_needed: The number of shares (``k``) from the erasure
+        encoding process which are required to reconstruct original data of
+        the indicated size.
+
+    :param int datasize: The size of the data to consider, in bytes.
+
+    :return int: The size of a single erasure encoding share for the given
+        inputs.
+    """
+    return div_ceil(datasize, shares_needed)
+
+
 def has_writes(tw_vectors):
     """
     :param tw_vectors: See
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 025260d8be245dd8c703ed30429a12c20cb6ba24..4384cacb8d946d8408cd3e4f5e3bb8f9f8f835a9 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -122,10 +122,36 @@ def _config_string_from_sections(divided_sections):
     ))
 
 
-def tahoe_config_texts(storage_client_plugins):
+def tahoe_config_texts(storage_client_plugins, shares):
     """
     Build the text of complete Tahoe-LAFS configurations for a node.
+
+    :param storage_client_plugins: A dictionary with storage client plugin
+        names as keys.
+
+    :param shares: A strategy to build erasure encoding parameters.  These are
+        built as a three-tuple giving (needed, total, happy).  Each element
+        may be an integer or None to leave it unconfigured (and rely on the
+        default).
     """
+    def merge_shares(shares, the_rest):
+        for (k, v) in zip(("needed", "happy", "total"), shares):
+            if v is not None:
+                the_rest["shares." + k] = u"{}".format(v)
+        return the_rest
+
+    client_section = builds(
+        merge_shares,
+        shares,
+        fixed_dictionaries(
+            {
+                "storage.plugins": just(
+                    u",".join(storage_client_plugins.keys()),
+                ),
+            },
+        ),
+    )
+
     return builds(
         lambda *sections: _config_string_from_sections(
             sections,
@@ -144,26 +170,23 @@ def tahoe_config_texts(storage_client_plugins):
                         "nickname": node_nicknames(),
                     },
                 ),
-                "client": fixed_dictionaries(
-                    {
-                        "storage.plugins": just(
-                            u",".join(storage_client_plugins.keys()),
-                        ),
-                    },
-                ),
+                "client": client_section,
             },
         ),
     )
 
 
-def minimal_tahoe_configs(storage_client_plugins=None):
+def minimal_tahoe_configs(storage_client_plugins=None, shares=just((None, None, None))):
     """
     Build complete Tahoe-LAFS configurations for a node.
+
+    :param shares: See ``tahoe_config_texts``.
     """
     if storage_client_plugins is None:
         storage_client_plugins = {}
     return tahoe_config_texts(
         storage_client_plugins,
+        shares,
     ).map(
         lambda config_text: lambda basedir, portnumfile: config_from_string(
             basedir,
@@ -287,14 +310,33 @@ def client_errorredeemer_configurations(details):
     })
 
 
-def tahoe_configs(zkapauthz_v1_configuration=client_dummyredeemer_configurations()):
+def tahoe_configs(
+        zkapauthz_v1_configuration=client_dummyredeemer_configurations(),
+        shares=just((None, None, None)),
+):
     """
     Build complete Tahoe-LAFS configurations including the zkapauthorizer
     client plugin section.
+
+    :param shares: See ``tahoe_config_texts``.
     """
     return minimal_tahoe_configs({
         u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration,
-    })
+    }, shares)
+
+
+def share_parameters():
+    """
+    Build three-tuples of integers giving usable k, happy, N parameters to
+    Tahoe-LAFS' erasure encoding process.
+    """
+    return lists(
+        integers(min_value=1, max_value=255),
+        min_size=3,
+        max_size=3,
+    ).map(
+        sorted,
+    )
 
 
 def vouchers():
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 0bbc9bb6418d335d1f93bb5712ace73301024b70..e13d4747e0c2c3246c4ce012b7da911df0970212 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -145,6 +145,10 @@ from ..resource import (
     from_configuration,
 )
 
+from ..pricecalculator import (
+    PriceCalculator,
+)
+
 from ..storage_common import (
     required_passes,
     get_configured_pass_value,
@@ -163,6 +167,7 @@ from .strategies import (
     requests,
     request_paths,
     api_auth_tokens,
+    share_parameters,
 )
 from .matchers import (
     Provides,
@@ -1508,15 +1513,25 @@ class CalculatePriceTests(TestCase):
         )
 
     @given(
-        tahoe_configs(),
+        # Make the share encoding parameters easily accessible without going
+        # through the Tahoe-LAFS configuration.
+        share_parameters().flatmap(
+            lambda params: tuples(
+                just(params),
+                tahoe_configs(shares=just(params)),
+            ),
+        ),
         api_auth_tokens(),
         lists(integers(min_value=0)),
     )
-    def test_calculated_price(self, get_config, api_auth_token, sizes):
+    def test_calculated_price(self, encoding_params_and_get_config, api_auth_token, sizes):
         """
         A well-formed request returns the price in ZKAPs as an integer and the
         storage period (the minimum allowed) that they pay for.
         """
+        encoding_params, get_config = encoding_params_and_get_config
+        shares_needed, shares_happy, shares_total = encoding_params
+
         config = get_config_with_api_token(
             self.useFixture(TempDir()),
             get_config,
@@ -1525,10 +1540,11 @@ class CalculatePriceTests(TestCase):
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
 
-        expected_price = required_passes(
-            get_configured_pass_value(config),
-            sizes,
-        )
+        expected_price = PriceCalculator(
+            shares_needed=shares_needed,
+            shares_total=shares_total,
+            pass_value=get_configured_pass_value(config),
+        ).calculate(sizes)
 
         self.assertThat(
             authorized_request(
diff --git a/src/_zkapauthorizer/tests/test_pricecalculator.py b/src/_zkapauthorizer/tests/test_pricecalculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..25eaa40d09102b66ffbb592d590497c33d1c8517
--- /dev/null
+++ b/src/_zkapauthorizer/tests/test_pricecalculator.py
@@ -0,0 +1,227 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 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.
+
+"""
+Tests for ``_zkapauthorizer.pricecalculator``.
+"""
+
+from functools import (
+    partial,
+)
+
+from testtools import (
+    TestCase,
+)
+from testtools.matchers import (
+    Equals,
+    GreaterThan,
+    IsInstance,
+    MatchesAll,
+)
+
+from hypothesis import (
+    given,
+)
+
+from hypothesis.strategies import (
+    integers,
+    lists,
+    tuples,
+)
+
+from ..pricecalculator import (
+    PriceCalculator,
+)
+
+from .strategies import (
+    sizes,
+    share_parameters,
+)
+from .matchers import (
+    greater_or_equal,
+)
+
+file_sizes = lists(sizes(), min_size=1)
+
+class PriceCalculatorTests(TestCase):
+    """
+    Tests for ``PriceCalculator``.
+    """
+    @given(
+        integers(min_value=1),
+        integers(min_value=1),
+        file_sizes,
+    )
+    def test_pass_value(self, pass_value, more_value, file_sizes):
+        """
+        The result of ``PriceCalculator.calculate`` increases or remains the same
+        as pass value decreases.
+        """
+        calculator = partial(PriceCalculator, shares_needed=1, shares_total=1)
+        less_value = calculator(pass_value=pass_value)
+        more_value = calculator(pass_value=pass_value + more_value)
+
+        less_value_price = less_value.calculate(file_sizes)
+        more_value_price = more_value.calculate(file_sizes)
+
+        self.assertThat(
+            less_value_price,
+            greater_or_equal(more_value_price),
+        )
+
+    @given(
+        integers(min_value=1, max_value=127),
+        integers(min_value=1, max_value=127),
+        file_sizes,
+    )
+    def test_shares_needed(self, shares_needed, more_needed, file_sizes):
+        """
+        The result of ``PriceCalculator.calculate`` never increases as
+        ``shares_needed`` increases.
+        """
+        calculator = partial(PriceCalculator, pass_value=100, shares_total=255)
+        fewer_needed = calculator(shares_needed=shares_needed)
+        more_needed = calculator(shares_needed=shares_needed + more_needed)
+
+        fewer_needed_price = fewer_needed.calculate(file_sizes)
+        more_needed_price = more_needed.calculate(file_sizes)
+
+        self.assertThat(
+            fewer_needed_price,
+            greater_or_equal(more_needed_price),
+        )
+
+
+    @given(
+        integers(min_value=1, max_value=127),
+        integers(min_value=1, max_value=127),
+        file_sizes,
+    )
+    def test_shares_total(self, shares_total, more_total, file_sizes):
+        """
+        The result of ``PriceCalculator.calculate`` always increases as
+        ``shares_total`` increases.
+        """
+        calculator = partial(PriceCalculator, pass_value=100, shares_needed=1)
+        fewer_total = calculator(shares_total=shares_total)
+        more_total = calculator(shares_total=shares_total + more_total)
+
+        fewer_total_price = fewer_total.calculate(file_sizes)
+        more_total_price = more_total.calculate(file_sizes)
+
+        self.assertThat(
+            more_total_price,
+            greater_or_equal(fewer_total_price),
+        )
+
+    @given(
+        integers(min_value=1, max_value=100).flatmap(
+            lambda num_files: tuples(
+                lists(sizes(), min_size=num_files, max_size=num_files),
+                lists(sizes(), min_size=num_files, max_size=num_files),
+            ),
+        ),
+        integers(min_value=1),
+        share_parameters(),
+    )
+    def test_file_sizes(self, file_sizes, pass_value, parameters):
+        """
+        The result of ``PriceCalculator.calculate`` never decreases as the values
+        of ``file_sizes`` increase.
+        """
+        smaller_sizes, increases = file_sizes
+        larger_sizes = list(a + b for (a, b) in zip(smaller_sizes, increases))
+        k, happy, N = parameters
+
+        calculator = PriceCalculator(
+            pass_value=pass_value,
+            shares_needed=k,
+            shares_total=N,
+        )
+
+        smaller_sizes_price = calculator.calculate(smaller_sizes)
+        larger_sizes_price = calculator.calculate(larger_sizes)
+
+        self.assertThat(
+            larger_sizes_price,
+            greater_or_equal(smaller_sizes_price),
+        )
+
+    @given(
+        integers(min_value=1),
+        share_parameters(),
+        file_sizes,
+    )
+    def test_positive_integer_price(self, pass_value, parameters, file_sizes):
+        """
+        The result of ``PriceCalculator.calculate`` for a non-empty size list is
+        always a positive integer.
+        """
+        k, happy, N = parameters
+        calculator = PriceCalculator(
+            pass_value=pass_value,
+            shares_needed=k,
+            shares_total=N,
+        )
+        price = calculator.calculate(file_sizes)
+        self.assertThat(
+            price,
+            MatchesAll(
+                IsInstance((int, long)),
+                GreaterThan(0),
+            ),
+        )
+
+    @given(
+        integers(min_value=1),
+        share_parameters(),
+        file_sizes,
+    )
+    def test_linear_increase(self, pass_value, parameters, file_sizes):
+        """
+        The result of ``PriceCalculator.calculate`` doubles if the file size list
+        is doubled.
+        """
+        k, happy, N = parameters
+        calculator = PriceCalculator(
+            pass_value=pass_value,
+            shares_needed=k,
+            shares_total=N,
+        )
+        smaller_price = calculator.calculate(file_sizes)
+        larger_price = calculator.calculate(file_sizes + file_sizes)
+        self.assertThat(
+            larger_price,
+            Equals(smaller_price * 2),
+        )
+
+    @given(
+        integers(min_value=1),
+    )
+    def test_one_pass(self, pass_value):
+        """
+        The result of ``PriceCalculator.calculate`` is exactly ``1`` if the amount
+        of data to be stored equals the value of a pass.
+        """
+        calculator = PriceCalculator(
+            pass_value=pass_value,
+            shares_needed=1,
+            shares_total=1,
+        )
+        price = calculator.calculate([pass_value])
+        self.assertThat(
+            price,
+            Equals(1),
+        )
diff --git a/src/_zkapauthorizer/tests/test_strategies.py b/src/_zkapauthorizer/tests/test_strategies.py
index 6a0307ea4b799204dba2c4f297473a0d52619215..3f60fcd8a617e344e20b7c5e2bb496143e07d571 100644
--- a/src/_zkapauthorizer/tests/test_strategies.py
+++ b/src/_zkapauthorizer/tests/test_strategies.py
@@ -34,6 +34,8 @@ from hypothesis import (
 )
 from hypothesis.strategies import (
     data,
+    one_of,
+    just,
 )
 
 from allmydata.client import (
@@ -42,6 +44,7 @@ from allmydata.client import (
 
 from .strategies import (
     tahoe_config_texts,
+    share_parameters,
 )
 
 class TahoeConfigsTests(TestCase):
@@ -54,7 +57,15 @@ class TahoeConfigsTests(TestCase):
         Configurations built by the strategy can be parsed.
         """
         tempdir = self.useFixture(TempDir())
-        config_text = data.draw(tahoe_config_texts({}))
+        config_text = data.draw(
+            tahoe_config_texts(
+                storage_client_plugins={},
+                shares=one_of(
+                    just((None, None, None)),
+                    share_parameters(),
+                ),
+            ),
+        )
         note(config_text)
         config_from_string(
             tempdir.join(b"tahoe.ini"),