diff --git a/lnbits/extensions/lndhub/README.md b/lnbits/extensions/lndhub/README.md
new file mode 100644
index 0000000..f567d54
--- /dev/null
+++ b/lnbits/extensions/lndhub/README.md
@@ -0,0 +1,6 @@
+
lndhub Extension
+*connect to your lnbits wallet from BlueWallet or Zeus*
+
+Lndhub has nothing to do with lnd, it is just the name of the HTTP/JSON protocol https://bluewallet.io/ uses to talk to their Lightning custodian server at https://lndhub.io/.
+
+Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNBits joins the same club.
diff --git a/lnbits/extensions/lndhub/__init__.py b/lnbits/extensions/lndhub/__init__.py
new file mode 100644
index 0000000..b886dce
--- /dev/null
+++ b/lnbits/extensions/lndhub/__init__.py
@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+
+lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates")
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/lndhub/config.json b/lnbits/extensions/lndhub/config.json
new file mode 100644
index 0000000..584acf0
--- /dev/null
+++ b/lnbits/extensions/lndhub/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "lndhub",
+ "short_description": "Access lnbits from BlueWallet or Zeus.",
+ "icon": "navigation",
+ "contributors": ["fiatjaf"]
+}
diff --git a/lnbits/extensions/lndhub/decorators.py b/lnbits/extensions/lndhub/decorators.py
new file mode 100644
index 0000000..5fbc033
--- /dev/null
+++ b/lnbits/extensions/lndhub/decorators.py
@@ -0,0 +1,25 @@
+from base64 import b64decode
+from flask import jsonify, g, request
+from functools import wraps
+
+from lnbits.core.crud import get_wallet_for_key
+
+
+def check_wallet(requires_admin=False):
+ def wrap(view):
+ @wraps(view)
+ def wrapped_view(**kwargs):
+ token = request.headers["Authorization"].split("Bearer ")[1]
+ key_type, key = b64decode(token).decode("utf-8").split(":")
+
+ if requires_admin and key_type != "admin":
+ return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
+
+ g.wallet = get_wallet_for_key(key, key_type)
+ if not g.wallet:
+ return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
+ return view(**kwargs)
+
+ return wrapped_view
+
+ return wrap
diff --git a/lnbits/extensions/lndhub/migrations.py b/lnbits/extensions/lndhub/migrations.py
new file mode 100644
index 0000000..3c90d94
--- /dev/null
+++ b/lnbits/extensions/lndhub/migrations.py
@@ -0,0 +1,2 @@
+def migrate():
+ pass
diff --git a/lnbits/extensions/lndhub/templates/lndhub/index.html b/lnbits/extensions/lndhub/templates/lndhub/index.html
new file mode 100644
index 0000000..36c8b7b
--- /dev/null
+++ b/lnbits/extensions/lndhub/templates/lndhub/index.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+ {% for wallet in g.user.wallets %}
+
+
+ lndhub://admin:{{wallet.adminkey}}@{% raw %}{{baseURL}}{% endraw %}/lndhub/ext/
+
+
+ {% endfor %}
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/lndhub/utils.py b/lnbits/extensions/lndhub/utils.py
new file mode 100644
index 0000000..3db6317
--- /dev/null
+++ b/lnbits/extensions/lndhub/utils.py
@@ -0,0 +1,21 @@
+from binascii import unhexlify
+
+from lnbits.bolt11 import Invoice
+
+
+def to_buffer(payment_hash: str):
+ return {"type": "Buffer", "data": [b for b in unhexlify(payment_hash)]}
+
+
+def decoded_as_lndhub(invoice: Invoice):
+ return {
+ "destination": invoice.payee,
+ "payment_hash": invoice.payment_hash,
+ "num_satoshis": invoice.amount_msat / 1000,
+ "timestamp": str(invoice.date),
+ "expiry": str(invoice.expiry),
+ "description": invoice.description,
+ "fallback_addr": "",
+ "cltv_expiry": invoice.min_final_cltv_expiry,
+ "route_hints": "",
+ }
diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py
new file mode 100644
index 0000000..aaf9202
--- /dev/null
+++ b/lnbits/extensions/lndhub/views.py
@@ -0,0 +1,11 @@
+from flask import render_template
+
+from lnbits.decorators import check_user_exists, validate_uuids
+from lnbits.extensions.lndhub import lndhub_ext
+
+
+@lndhub_ext.route("/")
+@validate_uuids(["usr"], required=True)
+@check_user_exists()
+def lndhub_index():
+ return render_template("lndhub/index.html")
diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py
new file mode 100644
index 0000000..1e9604b
--- /dev/null
+++ b/lnbits/extensions/lndhub/views_api.py
@@ -0,0 +1,186 @@
+import time
+from base64 import urlsafe_b64encode
+from flask import jsonify, g, request
+
+from lnbits.core.services import pay_invoice, create_invoice
+from lnbits.decorators import api_validate_post_request
+from lnbits import bolt11
+
+from lnbits.extensions.lndhub import lndhub_ext
+from .decorators import check_wallet
+from .utils import to_buffer, decoded_as_lndhub
+
+
+@lndhub_ext.route("/ext/getinfo", methods=["GET"])
+def lndhub_getinfo():
+ return jsonify({"error": True, "code": 1, "message": "bad auth"})
+
+
+@lndhub_ext.route("/ext/auth", methods=["POST"])
+@api_validate_post_request(
+ schema={
+ "login": {"type": "string", "required": True, "excludes": "refresh_token"},
+ "password": {"type": "string", "required": True, "excludes": "refresh_token"},
+ "refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]},
+ }
+)
+def lndhub_auth():
+ token = (
+ g.data["token"]
+ if "token" in g.data and g.data["token"]
+ else urlsafe_b64encode((g.data["login"] + ":" + g.data["password"]).encode("utf-8")).decode("ascii")
+ )
+ return jsonify({"refresh_token": token, "access_token": token})
+
+
+@lndhub_ext.route("/ext/addinvoice", methods=["POST"])
+@check_wallet()
+@api_validate_post_request(
+ schema={
+ "amt": {"type": "string", "required": True},
+ "memo": {"type": "string", "required": True},
+ "preimage": {"type": "string", "required": False},
+ }
+)
+def lndhub_addinvoice():
+ try:
+ _, pr = create_invoice(
+ wallet_id=g.wallet.id,
+ amount=int(g.data["amt"]),
+ memo=g.data["memo"],
+ )
+ except Exception as e:
+ return jsonify(
+ {
+ "error": True,
+ "code": 7,
+ "message": "Failed to create invoice: " + str(e),
+ }
+ )
+
+ invoice = bolt11.decode(pr)
+ return jsonify(
+ {
+ "pay_req": pr,
+ "payment_request": pr,
+ "add_index": "500",
+ "r_hash": to_buffer(invoice.payment_hash),
+ "hash": invoice.payment_hash,
+ }
+ )
+
+
+@lndhub_ext.route("/ext/payinvoice", methods=["POST"])
+@check_wallet(requires_admin=True)
+@api_validate_post_request(schema={"invoice": {"type": "string", "required": True}})
+def lndhub_payinvoice():
+ try:
+ pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["invoice"])
+ except Exception as e:
+ return jsonify(
+ {
+ "error": True,
+ "code": 10,
+ "message": "Payment failed: " + str(e),
+ }
+ )
+
+ invoice: bolt11.Invoice = bolt11.decode(g.data["invoice"])
+ return jsonify(
+ {
+ "payment_error": "",
+ "payment_preimage": "0" * 64,
+ "route": {},
+ "payment_hash": invoice.payment_hash,
+ "decoded": decoded_as_lndhub(invoice),
+ "fee_msat": 0,
+ "type": "paid_invoice",
+ "fee": 0,
+ "value": invoice.amount_msat / 1000,
+ "timestamp": int(time.time()),
+ "memo": invoice.description,
+ }
+ )
+
+
+@lndhub_ext.route("/ext/balance", methods=["GET"])
+@check_wallet()
+def lndhub_balance():
+ return jsonify({"BTC": {"AvailableBalance": g.wallet.balance}})
+
+
+@lndhub_ext.route("/ext/gettxs", methods=["GET"])
+@check_wallet()
+def lndhub_gettxs():
+ limit = int(request.args.get("limit", 200))
+
+ return jsonify(
+ [
+ {
+ "payment_preimage": payment.preimage,
+ "payment_hash": payment.payment_hash,
+ "fee_msat": payment.fee * 1000,
+ "type": "paid_invoice",
+ "fee": payment.fee,
+ "value": int(payment.amount / 1000),
+ "timestamp": payment.time,
+ "memo": payment.memo if not payment.pending else "Payment in transition",
+ }
+ for payment in g.wallet.get_payments(
+ pending=True, complete=True, outgoing=True, incoming=False, order="ASC"
+ )[0:limit]
+ ]
+ )
+
+
+@lndhub_ext.route("/ext/getuserinvoices", methods=["GET"])
+@check_wallet()
+def lndhub_getuserinvoices():
+ limit = int(request.args.get("limit", 200))
+
+ return jsonify(
+ [
+ {
+ "r_hash": to_buffer(invoice.payment_hash),
+ "payment_request": invoice.bolt11,
+ "add_index": "500",
+ "description": invoice.memo,
+ "payment_hash": invoice.payment_hash,
+ "ispaid": not invoice.pending,
+ "amt": int(invoice.amount / 1000),
+ "expire_time": int(time.time() + 1800),
+ "timestamp": invoice.time,
+ "type": "user_invoice",
+ }
+ for invoice in g.wallet.get_payments(
+ pending=True, complete=True, incoming=True, outgoing=False, order="ASC"
+ )[:limit]
+ ]
+ )
+
+
+@lndhub_ext.route("/ext/getbtc", methods=["GET"])
+@check_wallet()
+def lndhub_getbtc():
+ "load an address for incoming onchain btc"
+ return jsonify([])
+
+
+@lndhub_ext.route("/ext/getpending", methods=["GET"])
+@check_wallet()
+def lndhub_getpending():
+ "pending onchain transactions"
+ return jsonify([])
+
+
+@lndhub_ext.route("/ext/decodeinvoice", methods=["GET"])
+def lndhub_decodeinvoice():
+ invoice = request.args.get("invoice")
+ inv = bolt11.decode(invoice)
+ return jsonify(decoded_as_lndhub(inv))
+
+
+@lndhub_ext.route("/ext/checkrouteinvoice", methods=["GET"])
+def lndhub_checkrouteinvoice():
+ "not implemented on canonical lndhub"
+ pass