Make Money OnlineIntermediate16 min read

Build an AI-Powered Etsy Shop Pricing Tool with Open-Weight Models

Turn your costs and comparable listings you typed yourself into a break-even price and a profit-goal price. A small open-weight model can explain the sheet. It does not see Etsy, and it does not set the price.

Build an AI-Powered Etsy Shop Pricing Tool with Open-Weight Models

Difficulty: Medium · Required tools: Python, optional Ollama · Updated: October 2026

Overview

A price on Etsy has to clear two tests at once. After the fees, it has to leave you more than the wax, the box, the postage, and the hour you spent making the thing. Next to comparable listings, the buyer’s total has to look like it belongs. A model cannot see your invoices, and it is not allowed to browse Etsy for you. The useful split is: a script does the arithmetic, and a small open-weight model on your laptop only explains the sheet the script already printed.

This tutorial builds that sheet. You type your costs. You type the price and shipping of comparable listings you opened yourself. The script returns a break-even item price, a price that hits a profit goal you chose, and where those buyer totals sit against the middle of the comps. If Ollama is running with Llama 3.2, it writes a short note from that JSON. If it is not running, you still have the numbers.

The honest goal: a price you can defend with your own costs and a set of listings you actually looked at. The model does not train on your shop, does not scrape Etsy, and does not get a vote that overrides the floor.

A maker comparing one handmade candle with a short row of similar candles
A maker comparing one handmade candle with a short row of similar candles


Who This Is Useful For

Use this when you are about to publish one listing and you want the fee math written down before you pick a number.

  • Makers who know what the materials cost and still end up matching the cheapest listing they saw.
  • Sellers comparing “free shipping” shops with shops that charge postage, who need one number: what the buyer pays.
  • Anyone who was handed a prompt that says “suggest the optimal price” and wants to see which part is arithmetic and which part is a paragraph.
  • Skip it if you want a bot that watches competitors every hour, or a model trained on your past sales. Neither of those is this script. Etsy’s own stats, for your shop, live in Shop Manager. This sheet is for a listing you have not published yet.

    What You Will Learn

  • Which Etsy fees apply to the item price, and which ones also apply to the shipping you charge.
  • How to turn materials, labor, and postage into a break-even price.
  • How to compare that price with comps on what the buyer pays, not on the sticker alone.
  • Why a $200 listing should not drag the middle of a $20 set.
  • What a local open-weight model is allowed to say about a sheet it did not calculate.
  • What You Need

    Python 3.10 or newer. The script uses only the standard library: csv, json, statistics, and urllib. There is no pip install.

    A CSV you will type, named comps.csv. Five listings is the minimum the script accepts. Eight to fifteen, all actually similar to what you are selling, is the useful range. A “soy candle, 8 oz” is not comparable to a “candle-making kit” or a 20 oz three-wick.

    Ollama is optional. The model this article uses is llama3.2, the 3B text model, about 2GB on disk. That is a different download from ollama pull llama3, which is the older 8B model and a tight fit on an 8GB laptop. Install from ollama.com, then:

    bash
    ollama pull llama3.2
    

    Llama 3.2 is open-weight under Meta’s license. You are not training it. You are sending it one JSON object and asking for a short reading.


    Build the Pricer

    Step 1: Write the costs for one unit, including an hour rate

    Put a number on every thing that leaves the shop with that one sale. Materials and the box are obvious. Postage is what you pay the carrier, which is not always what you charge the buyer. Labor is the one people leave at zero, and then the floor looks friendly.

    For the sample run, the constants are: materials $8, packaging $1.50, half an hour at $20 (so $10 of labor), postage you pay $4.50, shipping you charge $4.50, and a profit goal of $12. Costs before fees are $24. Yours will differ. Change the constants. Do not leave the sample in place and then trust the price.

    Materials, a box, and a postage scale, the costs that have to clear before a price is real
    Materials, a box, and a postage scale, the costs that have to clear before a price is real


    Step 2: Type comparable listings into a CSV

    Open listings the way a buyer would. For each one, write the item price and the shipping shown for your own country. Skip shops that are a different product, a different size, or a digital file if you are shipping a physical one. Save this as comps.csv next to the script:

    csv
    price,shipping
    18.00,0
    22.00,5.00
    24.99,0
    19.50,4.00
    200.00,0
    16.00,6.00
    27.99,0
    22.00,0
    15.00,3.00
    

    The $200 row is in the sample on purpose. It should not set your price. The script sets aside buyer totals that sit far outside the middle, then builds the band from what remains.

    What the buyer pays is price + shipping. A $16 item with $6 shipping is a $22 purchase. A $22 item with free shipping is also a $22 purchase. Comparing stickers without shipping is how a free-shipping shop looks “expensive” when the buyer pays the same.

    A row of similar handmade mugs, the kind of set that belongs in one comparison
    A row of similar handmade mugs, the kind of set that belongs in one comparison


    Step 3: Use the published US fee schedule, then check it is still the one you pay

    The constants match Etsy’s public fee pages as checked for this article in October 2026, for a seller paid in the United States:

  • Listing fee: $0.20. Etsy charges it when you publish, whether or not the item sells. A listing lasts four months. This sheet counts one $0.20 per sale, which is right for an item that sells inside that window. Renewals of unsold listings are extra.
  • Transaction fee: 6.5% of the displayed item price plus the shipping and gift wrap you charge. For a US seller this fee is not charged on sales tax.
  • Payment processing, United States bank account: 3% + $0.25 of the total sale, and that total does include tax and shipping.
  • TAX_RATE defaults to 0, so the sheet is slightly optimistic: processing really is charged on sales tax the buyer pays and you do not keep. If your buyers cluster in one state, set TAX_RATE to that rate (0.08 for 8%) and the floor rises a little.

    Offsite Ads are not in the everyday floor. They apply only when Etsy’s off-site ad is what brought that order. Under $10,000 of sales in the prior 365 days the fee is 15% and you can opt out. At $10,000 it becomes 12%, participation is required, and the fee on one order is capped at $100. The script prints a what-if at 15%. It does not pretend every order pays it.

    Sellers outside the United States should replace PROCESSING_RATE and PROCESSING_FIXED from the country table in the Etsy Payments Policy. Canada and Australia domestic orders are not the US line. Some countries also add a regulatory operating fee or VAT on seller fees. If your fee page lists one, add it. Do not assume the US numbers traveled with you.

    One stack of coins divided so the sale and the amount you keep are not the same pile
    One stack of coins divided so the sale and the amount you keep are not the same pile
    Etsy Fees & Payments Policy

    Listing fee, the 6.5% transaction fee, and how Offsite Ads at 15% or 12% actually work.

    etsy.com


    Step 4: Run the sheet

    Save this as price_etsy.py beside comps.csv and run python3 price_etsy.py. On the sample file it prints a break-even item price of 22.52, a profit-goal item price of 35.78, and a net of 12.00 at that goal. Buyer totals are 27.02 and 40.28, because shipping of 4.50 is added on top. The $200 comp is set aside. The middle of the rest runs from 21.00 to 25.49, median 22.75. Both of your totals sit above that middle. If you do not get these figures, you are not running this sample.

    python
    #!/usr/bin/env python3
    """Price one listing from your costs and comps you typed yourself.

    Nothing is scraped. The local model, if it is running, only explains
    the sheet this script already calculated.
    """
    import csv
    import json
    import statistics
    import urllib.error
    import urllib.request
    from pathlib import Path

    --- your numbers. United States dollars. Change these. ---

    MATERIALS = 8.00 PACKAGING = 1.50 LABOR_HOURS = 0.5 HOURLY_RATE = 20.00 POSTAGE_YOU_PAY = 4.50 SHIPPING_YOU_CHARGE = 4.50 # 0 if free shipping is baked into the item price TARGET_PROFIT = 12.00

    United States Etsy Payments schedule. Confirm on Etsy's fee pages before you rely on it.

    Transaction fee: 6.5% of the displayed item price plus shipping you charge.

    Processing: 3% + $0.25 of item price plus shipping. Tax is ignored when TAX_RATE is 0.

    LISTING_FEE = 0.20 TRANSACTION_RATE = 0.065 PROCESSING_RATE = 0.03 PROCESSING_FIXED = 0.25 TAX_RATE = 0.0 OFFSITE_ADS_RATE = 0.15 # shown only as a what-if, not in the everyday floor

    COMPS = Path("comps.csv") # header: price,shipping
    OLLAMA_MODEL = "llama3.2"
    REPORT = Path("pricing-report.json")

    def money(value):
    return round(value + 1e-9, 2)

    def your_costs():
    return MATERIALS + PACKAGING + LABOR_HOURS * HOURLY_RATE + POSTAGE_YOU_PAY

    def keep_rate():
    # Share of (item price + shipping charged) left after the percentage fees.
    return 1 - TRANSACTION_RATE - PROCESSING_RATE * (1 + TAX_RATE)

    def net(item_price, extra_rate=0.0):
    base = item_price + SHIPPING_YOU_CHARGE
    fees = (
    LISTING_FEE
    + (TRANSACTION_RATE + extra_rate) * base
    + PROCESSING_RATE base (1 + TAX_RATE)
    + PROCESSING_FIXED
    )
    return base - fees - your_costs()

    def price_for_profit(profit, extra_rate=0.0):
    k = keep_rate() - extra_rate
    if k <= 0:
    raise SystemExit("Fee rates add up to 100% or more. Check the constants.")
    fixed = LISTING_FEE + PROCESSING_FIXED
    return (profit + your_costs() + fixed) / k - SHIPPING_YOU_CHARGE

    def load_comps(path):
    rows = []
    with path.open(newline="") as handle:
    reader = csv.DictReader(handle)
    if not reader.fieldnames or "price" not in reader.fieldnames:
    raise SystemExit("comps.csv needs a header row: price,shipping")
    for row in reader:
    price = float(row["price"])
    shipping = float(row.get("shipping") or 0)
    rows.append({
    "price": money(price),
    "shipping": money(shipping),
    "buyer_pays": money(price + shipping),
    })
    if len(rows) < 5:
    raise SystemExit("Type at least 5 comparable listings. A median of 2 is not a market.")
    return rows

    def band(values):
    ordered = sorted(values)
    if len(ordered) < 4:
    return ordered[0], statistics.median(ordered), ordered[-1], ordered, []
    q1, _, q3 = statistics.quantiles(ordered, n=4, method="inclusive")
    iqr = q3 - q1
    low_fence, high_fence = q1 - 1.5 iqr, q3 + 1.5 iqr
    kept = [v for v in ordered if low_fence <= v <= high_fence]
    dropped = [v for v in ordered if v < low_fence or v > high_fence]
    if len(kept) >= 4:
    k1, _, k3 = statistics.quantiles(kept, n=4, method="inclusive")
    return k1, statistics.median(kept), k3, kept, dropped
    return min(kept), statistics.median(kept), max(kept), kept, dropped

    def position(buyer_total, low, high):
    if buyer_total < low:
    return "below"
    if buyer_total > high:
    return "above"
    return "inside"

    def explain(summary):
    prompt = (
    "A seller already calculated this pricing sheet. "
    "Do not invent costs, fees, or competitor prices. Use only the JSON. "
    "In under 120 words, say whether the break-even total and the profit-goal total "
    "sit below, inside, or above the competitor middle, and name the single number "
    "in the sheet they would have to change to move a total that sits above the middle "
    "back inside it. If both totals are already inside or below, say the sheet does not "
    "ask them to cut anything. Do not claim you visited Etsy.\n\n"
    + json.dumps(summary, indent=2)
    )
    body = json.dumps({
    "model": OLLAMA_MODEL,
    "stream": False,
    "messages": [{"role": "user", "content": prompt}],
    }).encode()
    request = urllib.request.Request(
    "http://127.0.0.1:11434/api/chat",
    data=body,
    headers={"Content-Type": "application/json"},
    )
    try:
    with urllib.request.urlopen(request, timeout=120) as response:
    payload = json.loads(response.read().decode())
    except (urllib.error.URLError, TimeoutError) as exc:
    return f"Ollama is not running ({exc}). The numbers above are the result. Start it with: ollama pull {OLLAMA_MODEL}"
    return payload.get("message", {}).get("content", "").strip() or "The model returned an empty note."

    def main():
    comps = load_comps(COMPS)
    buyer_totals = [row["buyer_pays"] for row in comps]
    low, mid, high, kept, dropped = band(buyer_totals)
    break_even = price_for_profit(0)
    goal = price_for_profit(TARGET_PROFIT)
    break_even_total = break_even + SHIPPING_YOU_CHARGE
    goal_total = goal + SHIPPING_YOU_CHARGE
    summary = {
    "currency": "USD",
    "your_costs": money(your_costs()),
    "shipping_you_charge": money(SHIPPING_YOU_CHARGE),
    "fee_schedule": {
    "listing_fee": LISTING_FEE,
    "transaction_rate": TRANSACTION_RATE,
    "processing_rate": PROCESSING_RATE,
    "processing_fixed": PROCESSING_FIXED,
    "tax_rate_assumed": TAX_RATE,
    "note": "United States Etsy Payments. Offsite Ads is not included in the floor.",
    },
    "break_even_item_price": money(break_even),
    "break_even_buyer_pays": money(break_even_total),
    "profit_goal": money(TARGET_PROFIT),
    "goal_item_price": money(goal),
    "goal_buyer_pays": money(goal_total),
    "net_at_goal": money(net(goal)),
    "net_if_offsite_ads_also_apply_at_goal": money(net(goal, OFFSITE_ADS_RATE)),
    "comps_typed": len(comps),
    "comps_in_middle": len(kept),
    "outlier_buyer_totals_set_aside": [money(v) for v in dropped],
    "competitor_middle_buyer_pays": {
    "low": money(low),
    "median": money(mid),
    "high": money(high),
    },
    "break_even_vs_middle": position(break_even_total, low, high),
    "goal_vs_middle": position(goal_total, low, high),
    }
    REPORT.write_text(json.dumps(summary, indent=2) + "\n")
    print(json.dumps(summary, indent=2))
    print()
    print(explain(summary))

    if __name__ == "__main__":
    main()

    The same object is written to pricing-report.json. That file is the record. The paragraph underneath it is optional.

    The keep rate in the sample is 1 - 0.065 - 0.03 = 0.905. You keep about 90.5 cents of each dollar of item price plus shipping you charge, then you still pay the $0.20 listing fee, the $0.25 processing flat fee, and your $24 of costs. Solving that for $12 of profit is what produces $35.78. It is not a model guess. If net_at_goal is not your TARGET_PROFIT within a cent, the constants and the formula have drifted apart. Fix that before you read any prose.


    Step 5: Read “below”, “inside”, and “above” before you touch the listing

    Compare buyer totals with buyer totals. In the sample, the middle of the comps is a buyer total of about $21 to $25. Your break-even total is $27.02. Your profit-goal total is $40.28. Both are above the middle. Listing at $18 because several comps say 18 would not cover the $24 of costs plus fees. The sheet is telling you the comps, as typed, do not pay for this product at a $12 profit, and do not even pay for it at zero profit once labor is included.

    That is a useful answer. It is also allowed to mean the comps are the wrong comps: smaller candles, supplies instead of a finished good, a different country. Replace the CSV and run again. Do not ask the model to “find a sweeter price.”

    A product on the middle shelf, between a lower row and a higher row of similar goods
    A product on the middle shelf, between a lower row and a higher row of similar goods
    ReadingWhat it means for the listing
    Break-even above the middleThese comps do not cover your costs. Change the product, the costs, or the comp set.
    Break-even inside, goal aboveYou can list without a loss and still miss the profit you wrote down. Decide which one you are pricing for.
    Goal inside the middleThe profit you asked for already sits among these listings. You do not need a discount to “match the market.”
    Goal below the middleYou would be cheaper than most of this set even at your goal. Going lower is a choice, not a requirement.


    Step 6: Let the local model explain the sheet, and nothing else

    explain posts the JSON to http://127.0.0.1:11434/api/chat with stream off. The prompt forbids inventing costs, fees, or comps, caps the note at about 120 words, and tells the model not to claim it visited Etsy. The reply is message.content. Prices never go to a cloud API. They go to a process on your own machine, and only if that process is already running.

    If the note disagrees with break_even_vs_middle or goal_vs_middle, keep the JSON. Those two fields are comparisons of numbers you can recompute. The paragraph is not a second calculation.

    Llama 3.2 on Ollama

    The 3B open-weight model this script calls locally. About 2GB. It explains the sheet. It does not price the listing.

    ollama.com


    Step 7: Re-run when a cost changes, not on a timer

    Postage rates, a new supplier, and a profit goal you no longer believe are reasons to run the script again. A week passing is not. Etsy does not reprice your comps on a schedule this file knows about. You reprice the CSV when you have looked at new listings.

    Keep pricing-report.json with the listing draft. When you change TARGET_PROFIT or the hourly rate, the previous file is how you remember what you believed last time.


    Common Mistakes to Avoid

    Asking a model for “the optimal price.” Optimal is not a number a language model can see. The old shape of this tutorial pasted five prices into a prompt and treated the JSON that came back as a recommendation. Those prices were not a market, the prompt did not contain your postage, and nothing was trained. The script here will refuse to invent a third price between the floor and the comps. If the goal sits above the middle, the answer is that the goal sits above the middle.

    Scraping, or comparing stickers to buyer totals. Collecting listings by hand is slower. It is also the method that stays inside Etsy’s terms and that forces you to notice shipping. A shop at $16 with $6 shipping is not undercutting a shop at $22 with free shipping. And a scraper that adds time.sleep to “avoid a block” is a scraper with a nap. Do not build it.

    Leaving labor at zero, then calling the comps expensive. With labor set to $0 in this sample, costs fall by $10 and the floor drops with them. The comps did not get cheaper. You stopped paying yourself. That can be a real decision for a first batch. It should show up as a decision in the constants, not as a belief that $18 is what the product costs.

    Pricing under the cost of materials, next to a price that has already covered them
    Pricing under the cost of materials, next to a price that has already covered them

    Going Further

    Your country’s processing line. The US line is 3% + $0.25. The Etsy Payments Policy lists a different percent and flat fee by bank-account country, and a higher percent on some international orders. Swap the two constants. Leave the 6.5% transaction fee unless Etsy’s fee page has changed it.

    A currency that does not match your bank. If you list in a currency other than your payment-account currency, Etsy adds a 2.5% currency conversion fee on the sale amount. That is another extra_rate you can pass through price_for_profit the same way Offsite Ads is passed through net. Do not stack it unless you are actually listing in the other currency.

    A registered Etsy app, later. If you outgrow a CSV, the supported route is a registered app and the Open API, under the API terms: no mimicking checkout, no long-term copies of listing content, no scraping as a fallback when a key is annoying to get. This article stops at the CSV because that path needs no key and does not pretend to be a live feed.

    Key Takeaways

  • The price comes from your costs, the US fee schedule (or your country’s line), and comps you typed. A local model may explain that sheet. It does not produce it.
  • Compare what the buyer pays: item price plus shipping. Free shipping is not free to the fee calculation.
  • One $0.20 listing fee, 6.5% transaction, and 3% + $0.25 processing are the US defaults in the script. Confirm them on Etsy’s fee pages before you publish.
  • A far-off comp is set aside. It does not get to drag the middle.
  • If break-even is already above the middle, the next move is the costs or the comp set, not a more confident prompt.
  • Do not scrape Etsy. The terms forbid it, and this tool does not need it.
  • Sources

  • Etsy Fees & Payments Policy — $0.20 listing fee, 6.5% transaction fee, Offsite Ads at 15% or 12%.
  • Etsy Payments Policy — processing by country, including United States at 3% + $0.25, assessed on the total including tax and shipping.
  • Etsy Terms of Use — the ban on crawling, scraping, or spidering.
  • Etsy API Terms of Use and the Open API docs — the supported alternative to scraping, for a registered app.
  • Llama 3.2 on Ollama — the 3B open-weight model the optional note uses.
  • Learn AI, after work

    Track your progress, earn XP, and unlock more free tutorials in the AfterWork Bytes app.

    Open this tutorial in the app

    More AI tutorials