Part 1 built the report and part 2 added downloads. This part turns it into something you can charge for: a free preview, and a one-time payment that unlocks the rest.

The pattern

  1. Anyone can run an audit and see the score, the grade, and the title of the most important issue.
  2. The remaining fixes are hidden behind a button.
  3. The button starts a one-time payment. After paying, the visitor sees everything for that URL.

It works because the free part answers "does my site have a problem?" and the paid part answers "what exactly do I do about it?"

What you need

  • A Stripe account and its secret key
  • pip install stripe
  • A place to remember who has paid. This example uses a signed cookie so it stays short. A real app should store unlocks in a database, keyed by the buyer's email.

The code

import stripe
from flask import redirect, session, url_for

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
app.secret_key = os.environ["FLASK_SECRET"]
PRICE_CENTS = 1200


def unlocked(url: str) -> bool:
    return url in session.get("unlocked", [])


@app.post("/unlock")
def unlock():
    url = request.form["url"]
    checkout = stripe.checkout.Session.create(
        mode="payment",
        line_items=[{"quantity": 1, "price_data": {
            "currency": "usd", "unit_amount": PRICE_CENTS,
            "product_data": {"name": f"Full site audit: {url}"}}}],
        metadata={"url": url},
        success_url=url_for("paid", _external=True) + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=url_for("home", url=url, _external=True),
    )
    return redirect(checkout.url, code=303)


@app.get("/paid")
def paid():
    checkout = stripe.checkout.Session.retrieve(request.args["session_id"])
    if checkout.payment_status != "paid":
        return "Payment not completed", 402
    url = checkout.metadata["url"]
    session["unlocked"] = session.get("unlocked", []) + [url]
    return redirect(url_for("home", url=url))

And in home(), show one fix or all of them:

    fixes = data.get("priorities", [])
    shown = fixes if unlocked(url) else fixes[:1]
    hidden = len(fixes) - len(shown)
    cta = (f'<form method="post" action="/unlock"><input type="hidden" name="url" value="{escape(url)}">'
           f'<button>Unlock {hidden} more fixes: $12</button></form>') if hidden else ""

Render shown the way part 1 rendered priorities, and put cta under the list.

The three rules that keep this honest

Never send the hidden text to the browser. Blurring real content with CSS is not a paywall: anyone can read it in the page source. Send only what the visitor is allowed to see, and show a count for the rest.

Trust Stripe, not the redirect. The /paid route asks Stripe whether that session was paid. It does not take the browser's word for it. In production, also handle the checkout.session.completed webhook, because a buyer can close the tab before the redirect.

Take the URL from the payment, not the query string. The unlocked URL comes from checkout.metadata, which you set when the session was created. Otherwise one payment could be replayed to unlock any site.

Keep the free page useful

A preview that shows nothing gets no trust and no search traffic. Showing the real score, the grade and one real issue is enough for someone to judge whether the rest is worth paying for.

What you have

About 130 lines of Python across three posts: a URL form, a plain-English report, three export formats with your branding, and a paid unlock. The audit engine behind it runs 80+ checks per page, and the same key gives you accessibility audits, a tracker inventory and a multi-page crawl when you want to add more to the report.