diff --git a/lnbits/extensions/paywall/README.md b/lnbits/extensions/paywall/README.md
new file mode 100644
index 000000000..f3c20fb89
--- /dev/null
+++ b/lnbits/extensions/paywall/README.md
@@ -0,0 +1,11 @@
+
Example Extension
+*tagline*
+This is an example extension to help you organise and build you own.
+
+Try to include an image
+
+
+
+If your extension has API endpoints, include useful ones here
+
+curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "Grpc-Metadata-macaroon: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py
new file mode 100644
index 000000000..86e90f979
--- /dev/null
+++ b/lnbits/extensions/paywall/__init__.py
@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+
+paywall_ext = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json
new file mode 100644
index 000000000..8a763eafd
--- /dev/null
+++ b/lnbits/extensions/paywall/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Paywall",
+ "short_description": "BLah blah blah.",
+ "icon": "vpn_lock",
+ "contributors": ["eillarra"]
+}
diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py
new file mode 100644
index 000000000..55a874ff9
--- /dev/null
+++ b/lnbits/extensions/paywall/crud.py
@@ -0,0 +1,44 @@
+from base64 import urlsafe_b64encode
+from uuid import uuid4
+from typing import List, Optional, Union
+
+from lnbits.db import open_ext_db
+
+from .models import Paywall
+
+
+def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
+ with open_ext_db("paywall") as db:
+ paywall_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
+ db.execute(
+ """
+ INSERT INTO paywalls (id, wallet, url, memo, amount)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (paywall_id, wallet_id, url, memo, amount),
+ )
+
+ return get_paywall(paywall_id)
+
+
+def get_paywall(paywall_id: str) -> Optional[Paywall]:
+ with open_ext_db("paywall") as db:
+ row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
+
+ return Paywall(**row) if row else None
+
+
+def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ with open_ext_db("paywall") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
+
+ return [Paywall(**row) for row in rows]
+
+
+def delete_paywall(paywall_id: str) -> None:
+ with open_ext_db("paywall") as db:
+ db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))
diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py
new file mode 100644
index 000000000..4f12d7e6b
--- /dev/null
+++ b/lnbits/extensions/paywall/models.py
@@ -0,0 +1,10 @@
+from typing import NamedTuple
+
+
+class Paywall(NamedTuple):
+ id: str
+ wallet: str
+ url: str
+ memo: str
+ amount: int
+ time: int
diff --git a/lnbits/extensions/paywall/schema.sql b/lnbits/extensions/paywall/schema.sql
new file mode 100644
index 000000000..2f6ffb8fb
--- /dev/null
+++ b/lnbits/extensions/paywall/schema.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS paywalls (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ url TEXT NOT NULL,
+ memo TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
+);
diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html
new file mode 100644
index 000000000..8e25509a7
--- /dev/null
+++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lnbits/extensions/paywall/templates/paywall/index.html b/lnbits/extensions/paywall/templates/paywall/index.html
new file mode 100644
index 000000000..8b0af6ea9
--- /dev/null
+++ b/lnbits/extensions/paywall/templates/paywall/index.html
@@ -0,0 +1,222 @@
+{% extends "base.html" %}
+
+{% from "macros.jinja" import window_vars with context %}
+
+
+{% block page %}
+
+
+
+
+ New Paywall
+
+
+
+
+
+
+
+
Paywalls
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+ LNbits paywall extension
+
+
+
+
+ {% include "paywall/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create paywall
+ Cancel
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+ {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/paywall/templates/paywall/wall.html b/lnbits/extensions/paywall/templates/paywall/wall.html
new file mode 100644
index 000000000..423f41a7c
--- /dev/null
+++ b/lnbits/extensions/paywall/templates/paywall/wall.html
@@ -0,0 +1 @@
+{{ paywall.url }}
diff --git a/lnbits/extensions/paywall/views.py b/lnbits/extensions/paywall/views.py
new file mode 100644
index 000000000..4e8702df1
--- /dev/null
+++ b/lnbits/extensions/paywall/views.py
@@ -0,0 +1,21 @@
+from flask import g, abort, render_template
+
+from lnbits.decorators import check_user_exists, validate_uuids
+from lnbits.extensions.paywall import paywall_ext
+from lnbits.helpers import Status
+
+from .crud import get_paywall
+
+
+@paywall_ext.route("/")
+@validate_uuids(["usr"], required=True)
+@check_user_exists()
+def index():
+ return render_template("paywall/index.html", user=g.user)
+
+
+@paywall_ext.route("/")
+def wall(paywall_id):
+ paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.")
+
+ return render_template("paywall/wall.html", paywall=paywall)
diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py
new file mode 100644
index 000000000..11afd2114
--- /dev/null
+++ b/lnbits/extensions/paywall/views_api.py
@@ -0,0 +1,51 @@
+from flask import g, jsonify, request
+
+from lnbits.core.crud import get_user
+from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
+from lnbits.helpers import Status
+
+from lnbits.extensions.paywall import paywall_ext
+from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
+
+
+@paywall_ext.route("/api/v1/paywalls", methods=["GET"])
+@api_check_wallet_macaroon(key_type="invoice")
+def api_paywalls():
+ wallet_ids = [g.wallet.id]
+
+ if "all_wallets" in request.args:
+ wallet_ids = get_user(g.wallet.user).wallet_ids
+
+ return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK
+
+
+@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
+@api_check_wallet_macaroon(key_type="invoice")
+@api_validate_post_request(required_params=["url", "memo", "amount"])
+def api_paywall_create():
+ if not isinstance(g.data["amount"], int) or g.data["amount"] < 0:
+ return jsonify({"message": "`amount` needs to be a positive integer, or zero."}), Status.BAD_REQUEST
+
+ for var in ["url", "memo"]:
+ if not isinstance(g.data[var], str) or not g.data[var].strip():
+ return jsonify({"message": f"`{var}` needs to be a valid string."}), Status.BAD_REQUEST
+
+ paywall = create_paywall(wallet_id=g.wallet.id, url=g.data["url"], memo=g.data["memo"], amount=g.data["amount"])
+
+ return jsonify(paywall._asdict()), Status.CREATED
+
+
+@paywall_ext.route("/api/v1/paywalls/", methods=["DELETE"])
+@api_check_wallet_macaroon(key_type="invoice")
+def api_paywall_delete(paywall_id):
+ paywall = get_paywall(paywall_id)
+
+ if not paywall:
+ return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
+
+ if paywall.wallet != g.wallet.id:
+ return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN
+
+ delete_paywall(paywall_id)
+
+ return '', Status.NO_CONTENT