diff --git a/lnbits/extensions/paywall/README.md b/lnbits/extensions/paywall/README.md
new file mode 100644
index 00000000..738485e2
--- /dev/null
+++ b/lnbits/extensions/paywall/README.md
@@ -0,0 +1,22 @@
+# Paywall
+
+## Hide content behind a paywall, a user has to pay some amount to access your hidden content
+
+A Paywall is a way of restricting to content via a purchase or paid subscription. For example to read a determined blog post, or to continue reading further, to access a downloads area, etc...
+
+## Usage
+
+1. Create a paywall by clicking "NEW PAYWALL"\
+ 
+2. Fill the options for your PAYWALL
+ - select the wallet
+ - set the link that will be unlocked after a successful payment
+ - give your paywall a _Title_
+ - an optional small description
+ - and set an amount a user must pay to access the hidden content. Note this is the minimum amount, a user can over pay if they wish
+ - if _Remember payments_ is checked, a returning paying user won't have to pay again for the same content.\
+ 
+3. You can then use your paywall link to secure your awesome content\
+ 
+4. When a user wants to access your hidden content, he can use the minimum amount or increase and click the "_Check icon_" to generate an invoice, user will then be redirected to the content page\
+ 
diff --git a/lnbits/extensions/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py
new file mode 100644
index 00000000..cf9570a1
--- /dev/null
+++ b/lnbits/extensions/paywall/__init__.py
@@ -0,0 +1,12 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_paywall")
+
+paywall_ext: Blueprint = 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 00000000..d08ce7ba
--- /dev/null
+++ b/lnbits/extensions/paywall/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Paywall",
+ "short_description": "Create paywalls for content",
+ "icon": "policy",
+ "contributors": ["eillarra"]
+}
diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py
new file mode 100644
index 00000000..c13aba43
--- /dev/null
+++ b/lnbits/extensions/paywall/crud.py
@@ -0,0 +1,53 @@
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import Paywall
+
+
+async def create_paywall(
+ *,
+ wallet_id: str,
+ url: str,
+ memo: str,
+ description: Optional[str] = None,
+ amount: int = 0,
+ remembers: bool = True,
+) -> Paywall:
+ paywall_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO paywall.paywalls (id, wallet, url, memo, description, amount, remembers)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (paywall_id, wallet_id, url, memo, description, amount, int(remembers)),
+ )
+
+ paywall = await get_paywall(paywall_id)
+ assert paywall, "Newly created paywall couldn't be retrieved"
+ return paywall
+
+
+async def get_paywall(paywall_id: str) -> Optional[Paywall]:
+ row = await db.fetchone(
+ "SELECT * FROM paywall.paywalls WHERE id = ?", (paywall_id,)
+ )
+
+ return Paywall.from_row(row) if row else None
+
+
+async def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM paywall.paywalls WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Paywall.from_row(row) for row in rows]
+
+
+async def delete_paywall(paywall_id: str) -> None:
+ await db.execute("DELETE FROM paywall.paywalls WHERE id = ?", (paywall_id,))
diff --git a/lnbits/extensions/paywall/migrations.py b/lnbits/extensions/paywall/migrations.py
new file mode 100644
index 00000000..8afe58b1
--- /dev/null
+++ b/lnbits/extensions/paywall/migrations.py
@@ -0,0 +1,66 @@
+from sqlalchemy.exc import OperationalError # type: ignore
+
+
+async def m001_initial(db):
+ """
+ Initial paywalls table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE paywall.paywalls (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ secret TEXT NOT NULL,
+ url TEXT NOT NULL,
+ memo TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+
+async def m002_redux(db):
+ """
+ Creates an improved paywalls table and migrates the existing data.
+ """
+ await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old")
+ await db.execute(
+ """
+ CREATE TABLE paywall.paywalls (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ url TEXT NOT NULL,
+ memo TEXT NOT NULL,
+ description TEXT NULL,
+ amount INTEGER DEFAULT 0,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """,
+ remembers INTEGER DEFAULT 0,
+ extras TEXT NULL
+ );
+ """
+ )
+
+ for row in [
+ list(row) for row in await db.fetchall("SELECT * FROM paywall.paywalls_old")
+ ]:
+ await db.execute(
+ """
+ INSERT INTO paywall.paywalls (
+ id,
+ wallet,
+ url,
+ memo,
+ amount,
+ time
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (row[0], row[1], row[3], row[4], row[5], row[6]),
+ )
+
+ await db.execute("DROP TABLE paywall.paywalls_old")
diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py
new file mode 100644
index 00000000..d7f2451d
--- /dev/null
+++ b/lnbits/extensions/paywall/models.py
@@ -0,0 +1,23 @@
+import json
+
+from sqlite3 import Row
+from typing import NamedTuple, Optional
+
+
+class Paywall(NamedTuple):
+ id: str
+ wallet: str
+ url: str
+ memo: str
+ description: str
+ amount: int
+ time: int
+ remembers: bool
+ extras: Optional[dict]
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Paywall":
+ data = dict(row)
+ data["remembers"] = bool(data["remembers"])
+ data["extras"] = json.loads(data["extras"]) if data["extras"] else None
+ return cls(**data)
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 00000000..1157fa46
--- /dev/null
+++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html
@@ -0,0 +1,147 @@
+GET /paywall/api/v1/paywalls
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ [<paywall_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/paywalls -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ POST /paywall/api/v1/paywalls
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Body (application/json)
+ {"amount": <integer>, "description": <string>, "memo":
+ <string>, "remembers": <boolean>, "url":
+ <string>}
+
+ Returns 201 CREATED (application/json)
+
+ {"amount": <integer>, "description": <string>, "id":
+ <string>, "memo": <string>, "remembers": <boolean>,
+ "time": <int>, "url": <string>, "wallet":
+ <string>}
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/paywalls -d '{"url":
+ <string>, "memo": <string>, "description": <string>,
+ "amount": <integer>, "remembers": <boolean>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ POST
+ /paywall/api/v1/paywalls/<paywall_id>/invoice
+ Body (application/json)
+ {"amount": <integer>}
+
+ Returns 201 CREATED (application/json)
+
+ {"payment_hash": <string>, "payment_request":
+ <string>}
+ Curl example
+ curl -X POST {{ request.url_root
+ }}api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
+ <integer>}' -H "Content-type: application/json"
+
+ POST
+ /paywall/api/v1/paywalls/<paywall_id>/check_invoice
+ Body (application/json)
+ {"payment_hash": <string>}
+
+ Returns 200 OK (application/json)
+
+ {"paid": false}
+ {"paid": true, "url": <string>, "remembers":
+ <boolean>}
+ Curl example
+ curl -X POST {{ request.url_root
+ }}api/v1/paywalls/<paywall_id>/check_invoice -d
+ '{"payment_hash": <string>}' -H "Content-type: application/json"
+
+ DELETE
+ /paywall/api/v1/paywalls/<paywall_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+ Curl example
+ curl -X DELETE {{ request.url_root
+ }}api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+
{{ paywall.description }}
+ {% endif %} +
+ You can access the URL behind this paywall:
+ {% raw %}{{ redirectUrl }}{% endraw %}
+