diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md
new file mode 100644
index 00000000..d52547ae
--- /dev/null
+++ b/lnbits/extensions/satspay/README.md
@@ -0,0 +1,27 @@
+# SatsPay Server
+
+## Create onchain and LN charges. Includes webhooks!
+
+Easilly create invoices that support Lightning Network and on-chain BTC payment.
+
+1. Create a "NEW CHARGE"\
+ 
+2. Fill out the invoice fields
+ - set a descprition for the payment
+ - the amount in sats
+ - the time, in minutes, the invoice is valid for, after this period the invoice can't be payed
+ - set a webhook that will get the transaction details after a successful payment
+ - set to where the user should redirect after payment
+ - set the text for the button that will show after payment (not setting this, will display "NONE" in the button)
+ - select if you want onchain payment, LN payment or both
+ - depending on what you select you'll have to choose the respective wallets where to receive your payment\
+ 
+3. The charge will appear on the _Charges_ section\
+ 
+4. Your costumer/payee will get the payment page
+ - they can choose to pay on LN\
+ 
+ - or pay on chain\
+ 
+5. You can check the state of your charges in LNBits\
+ 
diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py
new file mode 100644
index 00000000..4bdaa2b6
--- /dev/null
+++ b/lnbits/extensions/satspay/__init__.py
@@ -0,0 +1,13 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_satspay")
+
+
+satspay_ext: Blueprint = Blueprint(
+ "satspay", __name__, static_folder="static", template_folder="templates"
+)
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/satspay/config.json b/lnbits/extensions/satspay/config.json
new file mode 100644
index 00000000..beb0071c
--- /dev/null
+++ b/lnbits/extensions/satspay/config.json
@@ -0,0 +1,8 @@
+{
+ "name": "SatsPay Server",
+ "short_description": "Create onchain and LN charges",
+ "icon": "payment",
+ "contributors": [
+ "arcbtc"
+ ]
+}
diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py
new file mode 100644
index 00000000..56cabdbe
--- /dev/null
+++ b/lnbits/extensions/satspay/crud.py
@@ -0,0 +1,130 @@
+from typing import List, Optional, Union
+
+# from lnbits.db import open_ext_db
+from . import db
+from .models import Charges
+
+from lnbits.helpers import urlsafe_short_hash
+
+from quart import jsonify
+import httpx
+from lnbits.core.services import create_invoice, check_invoice_status
+from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool
+
+
+###############CHARGES##########################
+
+
+async def create_charge(
+ user: str,
+ description: str = None,
+ onchainwallet: Optional[str] = None,
+ lnbitswallet: Optional[str] = None,
+ webhook: Optional[str] = None,
+ completelink: Optional[str] = None,
+ completelinktext: Optional[str] = "Back to Merchant",
+ time: Optional[int] = None,
+ amount: Optional[int] = None,
+) -> Charges:
+ charge_id = urlsafe_short_hash()
+ if onchainwallet:
+ wallet = await get_watch_wallet(onchainwallet)
+ onchain = await get_fresh_address(onchainwallet)
+ onchainaddress = onchain.address
+ else:
+ onchainaddress = None
+ if lnbitswallet:
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=lnbitswallet, amount=amount, memo=charge_id
+ )
+ else:
+ payment_hash = None
+ payment_request = None
+ await db.execute(
+ """
+ INSERT INTO satspay.charges (
+ id,
+ "user",
+ description,
+ onchainwallet,
+ onchainaddress,
+ lnbitswallet,
+ payment_request,
+ payment_hash,
+ webhook,
+ completelink,
+ completelinktext,
+ time,
+ amount,
+ balance
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ charge_id,
+ user,
+ description,
+ onchainwallet,
+ onchainaddress,
+ lnbitswallet,
+ payment_request,
+ payment_hash,
+ webhook,
+ completelink,
+ completelinktext,
+ time,
+ amount,
+ 0,
+ ),
+ )
+ return await get_charge(charge_id)
+
+
+async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE satspay.charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id)
+ )
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
+
+
+async def get_charge(charge_id: str) -> Charges:
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
+
+
+async def get_charges(user: str) -> List[Charges]:
+ rows = await db.fetchall(
+ """SELECT * FROM satspay.charges WHERE "user" = ?""", (user,)
+ )
+ return [Charges.from_row(row) for row in rows]
+
+
+async def delete_charge(charge_id: str) -> None:
+ await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
+
+
+async def check_address_balance(charge_id: str) -> List[Charges]:
+ charge = await get_charge(charge_id)
+ if not charge.paid:
+ if charge.onchainaddress:
+ mempool = await get_mempool(charge.user)
+ try:
+ async with httpx.AsyncClient() as client:
+ r = await client.get(
+ mempool.endpoint + "/api/address/" + charge.onchainaddress
+ )
+ respAmount = r.json()["chain_stats"]["funded_txo_sum"]
+ if respAmount >= charge.balance:
+ await update_charge(charge_id=charge_id, balance=respAmount)
+ except Exception:
+ pass
+ if charge.lnbitswallet:
+ invoice_status = await check_invoice_status(
+ charge.lnbitswallet, charge.payment_hash
+ )
+ if invoice_status.paid:
+ return await update_charge(charge_id=charge_id, balance=charge.amount)
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py
new file mode 100644
index 00000000..87446c80
--- /dev/null
+++ b/lnbits/extensions/satspay/migrations.py
@@ -0,0 +1,28 @@
+async def m001_initial(db):
+ """
+ Initial wallet table.
+ """
+
+ await db.execute(
+ """
+ CREATE TABLE satspay.charges (
+ id TEXT NOT NULL PRIMARY KEY,
+ "user" TEXT,
+ description TEXT,
+ onchainwallet TEXT,
+ onchainaddress TEXT,
+ lnbitswallet TEXT,
+ payment_request TEXT,
+ payment_hash TEXT,
+ webhook TEXT,
+ completelink TEXT,
+ completelinktext TEXT,
+ time INTEGER,
+ amount INTEGER,
+ balance INTEGER DEFAULT 0,
+ timestamp TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py
new file mode 100644
index 00000000..a7bfa14f
--- /dev/null
+++ b/lnbits/extensions/satspay/models.py
@@ -0,0 +1,39 @@
+from sqlite3 import Row
+from typing import NamedTuple
+import time
+
+
+class Charges(NamedTuple):
+ id: str
+ user: str
+ description: str
+ onchainwallet: str
+ onchainaddress: str
+ lnbitswallet: str
+ payment_request: str
+ payment_hash: str
+ webhook: str
+ completelink: str
+ completelinktext: str
+ time: int
+ amount: int
+ balance: int
+ timestamp: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Charges":
+ return cls(**dict(row))
+
+ @property
+ def time_elapsed(self):
+ if (self.timestamp + (self.time * 60)) >= time.time():
+ return False
+ else:
+ return True
+
+ @property
+ def paid(self):
+ if self.balance >= self.amount:
+ return True
+ else:
+ return False
diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html
new file mode 100644
index 00000000..526af7f3
--- /dev/null
+++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html
@@ -0,0 +1,171 @@
+
+ SatsPayServer, create Onchain/LN charges.
WARNING: If using with the
+ WatchOnly extension, we highly reccomend using a fresh extended public Key
+ specifically for SatsPayServer!
+
+ Created by, Ben Arc
+ POST /satspay/api/v1/charge
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/charge -d
+ '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
+
+ PUT
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/charge/<charge_id>
+ -d '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
+
+ GET
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/charge/<charge_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ GET /satspay/api/v1/charges
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ DELETE
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+ Curl example
+ curl -X DELETE {{ request.url_root
+ }}api/v1/charge/<charge_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ GET
+ /satspay/api/v1/charges/balance/<charge_id>
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+