
Volume Pricing
- 64 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Offers quantity-based price breaks so wholesale and bulk buyers automatically see lower prices as they add more units to the cart.
About
Implements tiered quantity pricing that discounts automatically as cart quantity rises. A developer uses it to serve wholesale and bulk-buyer pricing on a store.
- Quantity-based automatic price breaks
- Targets wholesale and bulk buyers
Volume Pricing by the numbers
- 64 all-time installs (skills.sh)
- Ranked #3,123 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill volume-pricingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Offers quantity-based price breaks so wholesale and bulk buyers automatically see lower prices as they add more units to the cart.
Files
Volume Pricing
Overview
Volume pricing — also called quantity breaks or tiered pricing — automatically reduces the unit price as order quantities increase. It is a core feature for B2B and wholesale channels, and an effective average-order-value driver for consumer stores ("Add 3 more for 10% off"). Most platforms do not include volume pricing natively; you need an app or plugin. This skill walks through setup on each platform and covers custom implementation for headless storefronts.
When to Use This Skill
- When selling to B2B buyers who expect per-unit price reductions for large orders
- When running a wholesale channel alongside a retail channel with separate pricing
- When you want to increase average order value by showing customers how much they save by buying more
- When managing multiple customer groups (retail, wholesale, distributor) with distinct price lists
- When building a configure-price-quote (CPQ) flow for custom bulk orders
Core Instructions
Step 1: Determine the merchant's platform and choose the right tool
| Platform | Recommended Tool | Why |
|---|---|---|
| Shopify | Wholesale Club or Quantity Breaks & Discounts by FORSBERG | Wholesale Club adds a full B2B channel with customer-group pricing; Quantity Breaks handles tiered pricing for B2C with a pricing table on product pages |
| Shopify Plus | Shopify B2B (built in) | Shopify Plus includes native B2B features including company accounts, price lists, and quantity minimums |
| WooCommerce | WooCommerce Dynamic Pricing extension (~$99/year) or YITH WooCommerce Dynamic Pricing & Discounts (~$70/year) | Both plugins support quantity-based tiers, customer-role-specific pricing, and product-page pricing tables |
| BigCommerce | Price Lists (native, Plus plan and above) | BigCommerce's Price Lists feature is designed for B2B volume pricing — assign price lists to customer groups |
| Custom / Headless | Build tier resolution logic calling your platform's pricing API | Full control over tier definitions and cart recalculation |
Step 2: Set up volume pricing on your platform
---
Shopify (non-Plus)
Option A: Quantity Breaks & Discounts app (B2C-focused)
1. Install Quantity Breaks & Discounts (by FORSBERG+two) from the Shopify App Store (free tier; paid plans ~$10/month) 2. In the app, go to Discount Groups → Create 3. Choose which products or collections to apply the tiers to 4. Add quantity tiers:
- Tier 1: Quantity 1+, 0% off (regular price)
- Tier 2: Quantity 5+, 10% off
- Tier 3: Quantity 10+, 20% off
- Tier 4: Quantity 25+, 30% off
5. The app automatically shows a pricing table on product pages and applies the correct discount when the customer updates their cart quantity
Option B: Wholesale Club (B2B-focused)
1. Install Wholesale Club from the Shopify App Store (~$30/month) 2. Create a wholesale customer group (the app creates a tag-based system) 3. Set wholesale prices: fixed prices or percentage discounts per product or collection 4. Tag wholesale customers with the wholesale tag to give them access 5. Wholesale customers see the tiered prices on product pages and at checkout
---
Shopify Plus — Native B2B
Shopify Plus includes a native B2B channel that replaces the need for a wholesale app:
1. Go to Sales channels → B2B 2. Create a Company for each B2B client 3. Under the company, create a Location and assign a Price list 4. In the price list, set prices per product (fixed prices or percentage adjustments) 5. Set Payment terms (net 30, net 60, etc.) and Order minimums 6. Company contacts log in through your store's B2B portal and see their assigned prices automatically
---
WooCommerce
Using WooCommerce Dynamic Pricing:
1. Install WooCommerce Dynamic Pricing from WooCommerce.com or YITH WooCommerce Dynamic Pricing & Discounts 2. Go to WooCommerce → Dynamic Pricing → Add Rule 3. Set:
- Rule type: Product Pricing or Category Pricing
- Apply to: specific products, categories, or all products
- Pricing type: percentage discount or fixed price override
4. Add quantity tiers:
| Minimum Qty | Maximum Qty | Discount |
|---|---|---|
| 1 | 4 | 0% |
| 5 | 9 | 10% |
| 10 | 24 | 20% |
| 25 | — | 30% |
5. Optionally restrict by User role for B2B/wholesale-only tiers 6. The plugin automatically recalculates prices when cart quantities change
Displaying a pricing table on product pages: Both Dynamic Pricing plugins include a pricing table widget that shows quantity break tiers directly on the product page. Configure its appearance in the plugin settings.
---
BigCommerce
BigCommerce's Price Lists feature handles volume pricing natively on Plus and above plans.
Setting up a price list with quantity tiers: 1. Go to Products → Price Lists → Create Price List 2. Name the list (e.g., "Wholesale 2026") and assign it to a Customer Group 3. Add products and set prices:
- For each product, you can set a base price override
- For quantity breaks, use the Bulk Pricing section on each product
4. Go to a product → Pricing tab → Bulk Pricing:
- Add tiers: e.g., 5+ units = $X per unit, 10+ units = $Y per unit
- Set whether the bulk price applies to all customers or a specific price list
5. Assign the price list to a customer group under Customers → Customer Groups
---
Custom / Headless
For headless storefronts, implement tier resolution in your pricing service:
interface PriceTier {
minQuantity: number;
maxQuantity?: number; // undefined = no upper limit
type: 'fixed' | 'percentage_off';
value: number; // cents if fixed; percentage (0-100) if percentage_off
customerGroup?: string; // null = all customers
}
async function resolveUnitPrice(
productId: string,
quantity: number,
customerGroup: string | null,
basePrice: number // cents
): Promise<{ unitPriceCents: number; savingsPct: number }> {
// 1. Check for customer-group-specific price list (highest priority)
if (customerGroup) {
const priceListItem = await db.priceLists
.findActive({ product_id: productId, customer_group: customerGroup, min_quantity: { lte: quantity } })
.orderBy('min_quantity', 'desc')
.first();
if (priceListItem) {
const savingsPct = Math.round((1 - priceListItem.price / basePrice) * 100);
return { unitPriceCents: priceListItem.price, savingsPct };
}
}
// 2. Find the best matching general volume tier
const tiers = await db.priceTiers.find({
product_id: productId,
customer_group: customerGroup ?? null,
});
const applicable = tiers.filter(t =>
quantity >= t.minQuantity && (t.maxQuantity === undefined || quantity <= t.maxQuantity)
).sort((a, b) => b.minQuantity - a.minQuantity); // highest-qualifying tier first
const tier = applicable[0];
if (!tier) return { unitPriceCents: basePrice, savingsPct: 0 };
const unitPriceCents = tier.type === 'fixed'
? tier.value
: Math.round(basePrice * (1 - tier.value / 100));
const savingsPct = Math.round((1 - unitPriceCents / basePrice) * 100);
return { unitPriceCents, savingsPct };
}Recalculate on every cart quantity change:
async function updateCartLinePricing(cartId: string, lineId: string, newQuantity: number) {
const line = await db.cartLines.findById(lineId);
const { unitPriceCents } = await resolveUnitPrice(
line.productId, newQuantity, line.customerGroup, line.basePriceCents
);
await db.cartLines.update(lineId, {
quantity: newQuantity,
unitPriceCents,
lineTotalCents: unitPriceCents * newQuantity,
});
}Show a pricing table on product pages:
Query the tier breakpoints and display the table:
async function getPricingTable(productId: string, customerGroup: string | null, basePrice: number) {
const breakpoints = [1, 5, 10, 25, 50, 100];
const rows = await Promise.all(breakpoints.map(async qty => {
const { unitPriceCents, savingsPct } = await resolveUnitPrice(productId, qty, customerGroup, basePrice);
return { quantity: qty, unitPriceCents, savingsPct };
}));
// Only show rows where the price actually changes
return rows.filter((row, i) => i === 0 || row.unitPriceCents !== rows[i - 1].unitPriceCents);
}Best Practices
- Show the pricing table on the product page — displaying upcoming tiers ("Add 3 more to get 10% off") is a proven AOV driver; do not hide this information
- Recalculate prices when cart quantity changes — ensure the unit price updates live in the cart, not just at checkout
- Keep price lists separate from tier logic — B2B price list overrides should take precedence over volume tier discounts; resolve them in a clear priority order
- Set minimum order quantities for wholesale tiers — if a wholesale price list requires a minimum order, enforce this at checkout, not just in the UI
- Validate minimum quantities on checkout — some B2B configurations require minimum quantities; enforce server-side to prevent orders that bypass the UI
- Display savings prominently — "You're saving $18.00 (30%)" is more compelling than showing only the discounted price
Common Pitfalls
| Problem | Solution |
|---|---|
| Cart quantity changes but price doesn't update | Recalculate all line item prices on every cart quantity change in real-time, not just at checkout |
| B2B buyer sees retail prices in order confirmation email | Pass customerGroup to all price resolution calls, including order confirmation email rendering |
| A customer in two groups gets inconsistent prices | Resolve by explicit priority: price list > product tier > category tier > base price |
| Pricing table shows stale tiers after an update | Clear your pricing cache when tiers are modified; invalidate by product or tier version |
| Tiered prices not shown on search results pages | Product cards on search/collection pages should also resolve prices server-side for logged-in B2B customers |
Related Skills
- @b2b-commerce
- @price-rules-engine
- @discount-engine
- @coupon-management
- @multi-channel-selling
{
"context": "Tests whether the agent implements the price resolution function with the correct priority ordering (price list beats tiers), correct percentage_off formula, and highest-priority tier selection for overlapping tiers.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Price list checked first",
"max_score": 12,
"description": "The resolveUnitPrice implementation checks price list entries BEFORE checking volume tiers (price list has higher priority)"
},
{
"name": "Price list returns early",
"max_score": 8,
"description": "When a matching price list item is found, the function returns immediately without evaluating volume tiers"
},
{
"name": "tierApplied = 'price_list'",
"max_score": 7,
"description": "When a price list item is used, the returned tierApplied value is 'price_list'"
},
{
"name": "Percentage_off formula",
"max_score": 12,
"description": "For percentage_off tiers, unit price is calculated as Math.round(basePrice * (1 - price_value / 100))"
},
{
"name": "Highest priority tier wins",
"max_score": 12,
"description": "When multiple tiers match a quantity, the tier with the highest priority value is selected (not the one with highest min_quantity or biggest discount)"
},
{
"name": "max_quantity boundary check",
"max_score": 8,
"description": "Tiers are filtered by both min_quantity (inclusive) and max_quantity (inclusive, null = unlimited) before priority sort"
},
{
"name": "No match returns base price",
"max_score": 7,
"description": "When no tier and no price list item matches, unitPrice equals basePrice and tierApplied is null"
},
{
"name": "customerGroup null safety",
"max_score": 7,
"description": "When customerGroup is null, price list lookup is skipped entirely (no null-dereference errors)"
},
{
"name": "Test: price list override",
"max_score": 7,
"description": "Test case verifies a B2B customer gets the price list price instead of the tier price"
},
{
"name": "Test: overlapping tier priority",
"max_score": 10,
"description": "Test case verifies that when two tiers overlap in quantity range, the higher-priority one is applied"
},
{
"name": "Prices stored as integers (cents)",
"max_score": 5,
"description": "Fixed prices and price list prices are handled as integer cent values in the implementation"
},
{
"name": "Priority ordering comment",
"max_score": 5,
"description": "Code includes a comment documenting the resolution priority order (price list > tiers > base price)"
}
]
}
Storefront Pricing Engine: Unit Price Resolver
Problem/Feature Description
A growing e-commerce platform needs a TypeScript pricing module that determines the correct unit price for any product in a shopping context. The platform has both retail and B2B customers. B2B customers belong to named customer groups (e.g., "wholesale", "distributor") and may have dedicated price agreements negotiated with account managers. All customers can also benefit from quantity breaks — the more they buy, the cheaper the unit price.
The pricing team has reported bugs where B2B customers sometimes get the wrong price because the system doesn't consistently apply their negotiated rates. The team needs a clean, correct implementation of a resolveUnitPrice function that handles all these cases deterministically. When multiple pricing rules could apply, behavior must be predictable and documented.
Output Specification
Produce a self-contained TypeScript file pricing.ts that:
- Exports a
resolveUnitPricefunction that accepts productId, quantity, customerGroup (nullable), and basePrice (in cents) - Returns
{ unitPrice: number; tierApplied: string | null } - Includes inline comments explaining the resolution priority order
- Uses a mock/stub db layer (you can define the data inline or as a simple in-memory structure) so the logic can be read without a real database
Also produce pricing.test.ts with at least 4 test cases covering: 1. A customer with a matching price list entry 2. A customer without a price list but with a matching volume tier 3. A customer where no tier applies (returns base price) 4. Overlapping tiers with different priorities (verify correct tier is selected)
You may use any test framework or plain assertion functions — the test file should be runnable with npx ts-node pricing.test.ts or node --loader ts-node/esm pricing.test.ts.
{
"context": "Tests whether the agent uses the specified breakpoints for the pricing table, filters out rows where the price doesn't change, calculates cart savings correctly, and displays savings prominently in the demo output.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Breakpoints [1,5,10,25,50,100,250]",
"max_score": 12,
"description": "getPricingTable evaluates prices at exactly these quantity breakpoints: 1, 5, 10, 25, 50, 100, 250"
},
{
"name": "250 labeled '250+'",
"max_score": 7,
"description": "The quantity 250 is labeled as '250+' in the pricing table output (not '250')"
},
{
"name": "Dedup unchanged price rows",
"max_score": 12,
"description": "getPricingTable filters out rows where unitPrice is the same as the previous row — only rows with a price change are returned"
},
{
"name": "lineTotal = unitPrice * quantity",
"max_score": 8,
"description": "calculateCart computes each line's total as unitPrice multiplied by quantity"
},
{
"name": "savings = (basePrice - unitPrice) * quantity",
"max_score": 10,
"description": "calculateCart computes per-line savings as (basePrice minus unitPrice) multiplied by quantity"
},
{
"name": "totalSavings returned",
"max_score": 8,
"description": "calculateCart returns a totalSavings field summing savings across all lines"
},
{
"name": "subtotal returned",
"max_score": 5,
"description": "calculateCart returns a subtotal field summing all lineTotals"
},
{
"name": "Savings shown in demo output",
"max_score": 10,
"description": "The demo script prints savings information (e.g. 'You save' or 'savings') — not just the unit price alone"
},
{
"name": "savingsPct in pricing table",
"max_score": 8,
"description": "getPricingTable includes a savings percentage field (e.g. savingsPct) calculated as Math.round((1 - unitPrice/basePrice) * 100)"
},
{
"name": "Pricing table first row always included",
"max_score": 10,
"description": "The first breakpoint row (quantity 1) is always included in the pricing table output regardless of price changes"
},
{
"name": "Demo uses sample tiers with 3+ breaks",
"max_score": 5,
"description": "The demo script defines a product with at least 3 distinct quantity break tiers (not just 1 or 2)"
},
{
"name": "Demo has 3-line cart",
"max_score": 5,
"description": "The demo script's calculateCart call uses a cart with at least 3 lines"
}
]
}
Retail Storefront: Volume Pricing Display and Cart Engine
Problem/Feature Description
An online office supplies retailer wants to increase average order value by showing customers exactly how much they can save by buying in bulk. Their marketing team has found that customers who see a "Buy 10 for $2.39 each — save 20%" nudge frequently upgrade their order. The engineering team needs to build two features: a product page pricing table that shows the available quantity break tiers, and a cart calculation engine that applies volume discounts and clearly surfaces the savings to the shopper.
The retailer currently shows only a single price on the product page and doesn't recalculate prices as the cart changes. They've had complaints from customers who changed their cart quantity but saw the wrong unit price. The new system must recalculate all line prices whenever quantities change, and the cart summary must show total savings so customers feel rewarded for buying more.
Output Specification
Produce a TypeScript file store-pricing.ts that exports:
1. A getPricingTable function that returns a list of rows for display on a product page, given a product's pricing tiers 2. A calculateCart function that takes a list of cart lines and returns each line's unit price, line total, and savings, plus the overall subtotal and total savings
Also produce store-pricing.demo.ts — a runnable demo script (executable with npx ts-node store-pricing.demo.ts) that:
- Defines a sample product with tiers (e.g., 10% off for 5+, 20% off for 10+, 30% off for 25+)
- Calls getPricingTable and prints the resulting rows to stdout
- Defines a sample 3-line cart and calls calculateCart, printing each line's details and the cart summary to stdout
The demo output should be readable — print prices in dollars (e.g. "$2.99") not raw cents.
{
"context": "Tests whether the agent correctly implements the volume pricing database schema with the right table structure, column types, nullable fields, constraints, and indexing as specified in the skill.",
"type": "weighted_checklist",
"checklist": [
{
"name": "price_tiers table exists",
"max_score": 5,
"description": "schema.sql contains a CREATE TABLE statement for price_tiers"
},
{
"name": "price_tiers UUID PK",
"max_score": 5,
"description": "price_tiers.id uses UUID type with DEFAULT gen_random_uuid()"
},
{
"name": "Nullable product_id and category_id",
"max_score": 7,
"description": "price_tiers.product_id and category_id are nullable (no NOT NULL constraint), referencing products and categories respectively"
},
{
"name": "Nullable customer_group",
"max_score": 5,
"description": "price_tiers.customer_group is nullable VARCHAR (no NOT NULL constraint)"
},
{
"name": "price_type CHECK constraint",
"max_score": 8,
"description": "price_tiers.price_type has a CHECK constraint limiting values to 'fixed' and 'percentage_off'"
},
{
"name": "price_value NUMERIC type",
"max_score": 7,
"description": "price_tiers.price_value uses NUMERIC(10,2) type (not INTEGER or FLOAT)"
},
{
"name": "priority column default",
"max_score": 5,
"description": "price_tiers.priority has NOT NULL with DEFAULT 0"
},
{
"name": "Lookup index",
"max_score": 10,
"description": "A CREATE INDEX is present on price_tiers covering (product_id, customer_group, min_quantity)"
},
{
"name": "price_lists table exists",
"max_score": 8,
"description": "schema.sql contains a CREATE TABLE statement for price_lists with name, customer_group, currency, starts_at, ends_at, is_active columns"
},
{
"name": "currency default USD",
"max_score": 5,
"description": "price_lists.currency has DEFAULT 'USD'"
},
{
"name": "is_active default true",
"max_score": 5,
"description": "price_lists.is_active has NOT NULL DEFAULT true"
},
{
"name": "price_list_items table exists",
"max_score": 8,
"description": "schema.sql contains a CREATE TABLE for price_list_items with price_list_id FK, product_id FK, price, and min_quantity columns"
},
{
"name": "price_list_items.price is INTEGER",
"max_score": 10,
"description": "price_list_items.price column uses INTEGER type (storing cents), NOT NUMERIC or DECIMAL"
},
{
"name": "min_quantity default 1",
"max_score": 7,
"description": "price_list_items.min_quantity has NOT NULL DEFAULT 1"
},
{
"name": "TIMESTAMPTZ timestamps",
"max_score": 5,
"description": "Timestamp columns (created_at, starts_at, ends_at) use TIMESTAMPTZ type (not TIMESTAMP without timezone)"
}
]
}
B2B Wholesale Platform: Volume Pricing Database Schema
Problem/Feature Description
A mid-sized industrial supply company is launching a new B2B e-commerce platform. They sell to three customer segments: retail consumers, wholesale resellers, and distributors. Each segment negotiates different pricing, and the company wants a system where wholesale and distributor accounts can be assigned dedicated price lists that override standard pricing — while all customers can still benefit from quantity breaks (e.g., buying 10 units gets a cheaper unit price than buying 1).
The engineering team needs a PostgreSQL schema to power this pricing system. The schema must handle: product-level quantity tiers, category-level tiers, customer-group-specific overrides, and named price lists for B2B accounts. The ops team has already confirmed that the products and categories tables exist.
Output Specification
Produce a file schema.sql containing the complete PostgreSQL DDL (CREATE TABLE and CREATE INDEX statements) for the volume pricing and B2B price list system.
Also produce a short notes.md explaining any design decisions made (e.g., column types, nullable fields, indexing strategy).
{
"name": "finsi/volume-pricing",
"version": "0.1.0",
"summary": "Quantity-based price breaks, tiered pricing tables, and B2B price lists",
"skills": {
"volume-pricing": {
"path": "SKILL.md"
}
}
}