diff --git a/lnbits/extensions/nostrnip5/README.md b/lnbits/extensions/nostrnip5/README.md
new file mode 100644
index 00000000..2b5bd538
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/README.md
@@ -0,0 +1,19 @@
+# Invoices
+
+## Create invoices that you can send to your client to pay online over Lightning.
+
+This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.
+
+## Usage
+
+1. Create an invoice by clicking "NEW INVOICE"\
+ 
+2. Fill the options for your INVOICE
+ - select the wallet
+ - select the fiat currency the invoice will be denominated in
+ - select a status for the invoice (default is draft)
+ - enter a company name, first name, last name, email, phone & address (optional)
+ - add one or more line items
+ - enter a name & price for each line item
+3. You can then use share your invoice link with your customer to receive payment\
+ 
\ No newline at end of file
diff --git a/lnbits/extensions/nostrnip5/__init__.py b/lnbits/extensions/nostrnip5/__init__.py
new file mode 100644
index 00000000..a9a2ea1c
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/__init__.py
@@ -0,0 +1,36 @@
+import asyncio
+
+from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
+
+db = Database("ext_nostrnip5")
+
+nostrnip5_static_files = [
+ {
+ "path": "/nostrnip5/static",
+ "app": StaticFiles(directory="lnbits/extensions/nostrnip5/static"),
+ "name": "nostrnip5_static",
+ }
+]
+
+nostrnip5_ext: APIRouter = APIRouter(prefix="/nostrnip5", tags=["nostrnip5"])
+
+
+def nostrnip5_renderer():
+ return template_renderer(["lnbits/extensions/nostrnip5/templates"])
+
+
+from .tasks import wait_for_paid_invoices
+
+
+def nostrnip5_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+
+
+from .views import * # noqa
+from .views_api import * # noqa
diff --git a/lnbits/extensions/nostrnip5/config.json b/lnbits/extensions/nostrnip5/config.json
new file mode 100644
index 00000000..658723aa
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Nostr NIP-5",
+ "short_description": "Verify addresses for Nostr NIP-5",
+ "icon": "request_quote",
+ "contributors": ["leesalminen"]
+}
diff --git a/lnbits/extensions/nostrnip5/crud.py b/lnbits/extensions/nostrnip5/crud.py
new file mode 100644
index 00000000..eff99074
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/crud.py
@@ -0,0 +1,162 @@
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import (
+ CreateDomainData,
+ Domain,
+ Address,
+ CreateAddressData,
+)
+
+
+async def get_domain(domain_id: str) -> Optional[Domain]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrnip5.domains WHERE id = ?", (domain_id,)
+ )
+ return Domain.from_row(row) if row else None
+
+async def get_domain_by_name(domain: str) -> Optional[Domain]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrnip5.domains WHERE domain = ?", (domain,)
+ )
+ return Domain.from_row(row) if row else None
+
+async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domain]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM nostrnip5.domains WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Domain.from_row(row) for row in rows]
+
+async def get_address(domain_id: str, address_id: str) -> Optional[Address]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrnip5.addresses WHERE domain_id = ? AND id = ?", (domain_id,address_id,)
+ )
+ return Address.from_row(row) if row else None
+
+async def get_address_by_local_part(domain_id: str, local_part: str) -> Optional[Address]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrnip5.addresses WHERE domain_id = ? AND local_part = ?", (domain_id,local_part,)
+ )
+ return Address.from_row(row) if row else None
+
+async def get_addresses(domain_id: str) -> List[Address]:
+ rows = await db.fetchall(
+ f"SELECT * FROM nostrnip5.addresses WHERE domain_id = ?", (domain_id,)
+ )
+
+ return [Address.from_row(row) for row in rows]
+
+async def get_all_addresses(wallet_ids: Union[str, List[str]]) -> List[Address]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"""
+ SELECT a.*
+ FROM nostrnip5.addresses a
+ JOIN nostrnip5.domains d ON d.id = a.domain_id
+ WHERE d.wallet IN ({q})
+ """,
+ (*wallet_ids,)
+ )
+
+ return [Address.from_row(row) for row in rows]
+
+async def activate_domain(domain_id: str, address_id: str) -> Address:
+ await db.execute(
+ """
+ UPDATE nostrnip5.addresses
+ SET active = true
+ WHERE domain_id = ?
+ AND id = ?
+ """,
+ (
+ domain_id,
+ address_id,
+ ),
+ )
+
+ address = await get_address(domain_id, address_id)
+ assert address, "Newly updated address couldn't be retrieved"
+ return address
+
+async def delete_domain(domain_id) -> bool:
+ await db.execute(
+ """
+ DELETE FROM nostrnip5.addresses WHERE domain_id = ?
+ """,
+ (
+ domain_id,
+ ),
+ )
+
+ await db.execute(
+ """
+ DELETE FROM nostrnip5.domains WHERE id = ?
+ """,
+ (
+ domain_id,
+ ),
+ )
+
+ return True
+
+async def delete_address(address_id) -> bool:
+ await db.execute(
+ """
+ DELETE FROM nostrnip5.addresses WHERE id = ?
+ """,
+ (
+ address_id,
+ ),
+ )
+
+async def create_address_internal(domain_id: str, data: CreateAddressData) -> Address:
+ address_id = urlsafe_short_hash()
+
+ await db.execute(
+ """
+ INSERT INTO nostrnip5.addresses (id, domain_id, local_part, pubkey, active)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ address_id,
+ domain_id,
+ data.local_part,
+ data.pubkey,
+ False,
+ ),
+ )
+
+ address = await get_address(domain_id, address_id)
+ assert address, "Newly created address couldn't be retrieved"
+ return address
+
+async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
+ domain_id = urlsafe_short_hash()
+
+ await db.execute(
+ """
+ INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ domain_id,
+ wallet_id,
+ data.currency,
+ int(data.amount * 100),
+ data.domain
+ ),
+ )
+
+ domain = await get_domain(domain_id)
+ assert domain, "Newly created domain couldn't be retrieved"
+ return domain
\ No newline at end of file
diff --git a/lnbits/extensions/nostrnip5/migrations.py b/lnbits/extensions/nostrnip5/migrations.py
new file mode 100644
index 00000000..f1ce4ca9
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/migrations.py
@@ -0,0 +1,35 @@
+async def m001_initial_invoices(db):
+
+ await db.execute(
+ f"""
+ CREATE TABLE nostrnip5.domains (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+
+ currency TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+
+ domain TEXT NOT NULL,
+
+ time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
+ );
+ """
+ )
+
+ await db.execute(
+ f"""
+ CREATE TABLE nostrnip5.addresses (
+ id TEXT PRIMARY KEY,
+ domain_id TEXT NOT NULL,
+
+ local_part TEXT NOT NULL,
+ pubkey TEXT NOT NULL,
+
+ active BOOLEAN NOT NULL DEFAULT false,
+
+ time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
+
+ FOREIGN KEY(domain_id) REFERENCES {db.references_schema}domains(id)
+ );
+ """
+ )
\ No newline at end of file
diff --git a/lnbits/extensions/nostrnip5/models.py b/lnbits/extensions/nostrnip5/models.py
new file mode 100644
index 00000000..31010639
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/models.py
@@ -0,0 +1,42 @@
+from enum import Enum
+from sqlite3 import Row
+from typing import List, Optional
+
+from fastapi.param_functions import Query
+from pydantic import BaseModel
+
+class CreateAddressData(BaseModel):
+ domain_id: str
+ local_part: str
+ pubkey: str
+ active: bool = False
+
+class CreateDomainData(BaseModel):
+ wallet: str
+ currency: str
+ amount: float = Query(..., ge=0.01)
+ domain: str
+
+class Domain(BaseModel):
+ id: str
+ wallet: str
+ currency: str
+ amount: int
+ domain: str
+ time: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Domain":
+ return cls(**dict(row))
+
+class Address(BaseModel):
+ id: str
+ domain_id: str
+ local_part: str
+ pubkey: str
+ active: bool
+ time: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Address":
+ return cls(**dict(row))
\ No newline at end of file
diff --git a/lnbits/extensions/nostrnip5/static/css/signup.css b/lnbits/extensions/nostrnip5/static/css/signup.css
new file mode 100644
index 00000000..e69de29b
diff --git a/lnbits/extensions/nostrnip5/tasks.py b/lnbits/extensions/nostrnip5/tasks.py
new file mode 100644
index 00000000..d27682c4
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/tasks.py
@@ -0,0 +1,33 @@
+import asyncio
+import json
+
+from lnbits.core.models import Payment
+from lnbits.helpers import urlsafe_short_hash
+from lnbits.tasks import internal_invoice_queue, register_invoice_listener
+
+from .crud import (
+ get_domain,
+ activate_domain,
+)
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ if payment.extra.get("tag") != "nostrnip5":
+ # not relevant
+ return
+
+ domain_id = payment.extra.get("domain_id")
+ address_id = payment.extra.get("address_id")
+
+ active = await activate_domain(domain_id, address_id)
+
+ return
diff --git a/lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html b/lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
new file mode 100644
index 00000000..6e2a6355
--- /dev/null
+++ b/lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
@@ -0,0 +1,153 @@
+GET /invoices/api/v1/invoices
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ [<invoice_object>, ...]
+ Curl example
+ curl -X GET {{ request.base_url }}invoices/api/v1/invoices -H
+ "X-Api-Key: <invoice_key>"
+
+ GET
+ /invoices/api/v1/invoice/{invoice_id}
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ {invoice_object}
+ Curl example
+ curl -X GET {{ request.base_url
+ }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
+ <invoice_key>"
+
+ POST /invoices/api/v1/invoice
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ {invoice_object}
+ Curl example
+ curl -X POST {{ request.base_url }}invoices/api/v1/invoice -H
+ "X-Api-Key: <invoice_key>"
+
+ POST
+ /invoices/api/v1/invoice/{invoice_id}
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ {invoice_object}
+ Curl example
+ curl -X POST {{ request.base_url
+ }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
+ <invoice_key>"
+
+ POST
+ /invoices/api/v1/invoice/{invoice_id}/payments
+ Headers
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ {payment_object}
+ Curl example
+ curl -X POST {{ request.base_url
+ }}invoices/api/v1/invoice/{invoice_id}/payments -H "X-Api-Key:
+ <invoice_key>"
+
+ GET
+ /invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}
+ Headers
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ Curl example
+ curl -X GET {{ request.base_url
+ }}invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash} -H
+ "X-Api-Key: <invoice_key>"
+
+
You can use this page to get NIP-5 verified on the nostr protocol under the {{ domain.domain }} domain.
+The current price is {{ "{:0,.2f}".format(domain.amount / 100) }} {{ domain.currency }} for a lifetime account.
+ +After submitting payment, your address will be
+ +and will be tied to this nostr pubkey
+ +