This is the full file; follow it from Step 0.

OpenReceive agent directions (Flask)

# READ THIS FIRST: this file is 43 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/flask/full.md
# Skip the download only if you already have all of it: pasted, read from disk or fetched raw.

Step 0 in brief (Step 0 below has the details):

These directions describe OpenReceive 0.4.24.

Add OpenReceive to a Flask application — the app you are already working in. You do not need a copy of the OpenReceive source: the engine is on PyPI (openreceive[sqlalchemy]), the checkout’s script and stylesheet ship inside that same package, and the quickstart is appended to this file in full, so you can do the whole integration without fetching anything. There is no openreceive.flask package: the quickstart’s openreceive_flask.py is the Flask integration. Copy it into this app as it is, and do not reimplement wallet RPC, settlement, pricing or the routes it serves.

Do not clone the OpenReceive repository into this app, and do not copy the starter’s models (Order, a session visitor, a one-product catalog) over tables that already exist. Find this application’s order, product, and user models — whatever they are actually named — and map the three hooks onto those.

Keep this application’s templates, authentication and database. Jinja pages get the <openreceive-checkout> element, served from the installed openreceive package as the quickstart shows; only an app that already has a JavaScript build uses @openreceive/react, /vue, /svelte or /angular instead. Do not add React or npm to a Jinja app. The login this app already has (Flask-Login’s current_user, or an id in Flask’s session) is what authorize checks, and the database OpenReceive gets is a sync SQLAlchemy Engine for the SAME database this app already uses.

What OpenReceive is

A payment library that runs inside YOUR server. It is a Flask Blueprint (openreceive.blueprint(), registered under /openreceive) that serves its HTTP routes inside the application you are editing, issues Lightning invoices against a wallet the merchant already controls, and calls back into your code when one settles. There is no OpenReceive account and no API key, and OpenReceive never holds the funds — the sats land in the wallet the merchant connected.

The one required credential is a receive-only NWC code (Nostr Wallet Connect): a string from the merchant’s wallet that can create invoices and read their status, and cannot spend. A swap provider (an “LSC” code) optionally lets the payer send USDT, USDC, ETH or SOL instead, converted into that same Lightning payment. Step 0 collects those credentials from the user; you write three hooks — authorize, amount_for, on_paid; OpenReceive supplies invoices, polling, settlement and the checkout UI. It never owns orders, users, prices, or fulfillment.

Step 0 — ask for the two codes, one question at a time

Before anything else, check one file: the project’s .env, if it exists. grep -E '^(NWC_URI|LSC_URI_PRIMARY)=.' .env | cut -d= -f1 names the codes it sets without printing them. Check with that, here and after each write, not by reading the file, and never print the values. A code that is already set is not asked for again; if both are set, skip to step 5 (making the server load the file), then the quickstart.

Otherwise your next action is a question to the user. Do not install packages, edit the app, write .env.example, or search anywhere else before asking it. Do not read deploy config (compose files, platform secrets), Docker containers, or process environments, and never run printenv. A new shop has neither code yet: hunting for one burns the turn and can leak other secrets into the transcript.

Two server-only URIs are needed before the quickstart:

The user never edits an environment file. They paste each code into the chat; you store it. Ask one question per message.

  1. First message — the NWC code, and nothing else. Ask for it and walk them through getting it:

    To receive payments I need a receive-only wallet code. In Rizful: open the menu, tap NWC, choose Receive-only NWC code, and tap Copy (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would rather run your own wallet, Alby Hub works too: Connections → Add Connection → Read Only. Paste the code here and I will store it.

    Do not mention .env, exports, or “tell me when it’s set”.

  2. When they paste it. If it does not start with nostr+walletconnect://, ask them to copy the receive-only code again. Otherwise write NWC_URI=<paste> into the project’s .env with your file-editing tool, creating the file if needed. Never write it with a shell command (echo, printf, a heredoc, python -c): the command line shows the code. Make sure .gitignore covers .env (and .dockerignore, if the app has one). Never echo the value, commit it, or put it in client code. Reply only that it is saved, then ask the next question.
  3. Second message — swaps. If the user asked for stablecoins, USDT, USDC, ETH, SOL, altcoins or “crypto” (as in “Bitcoin and stablecoin payments”), this message IS the walkthrough below: send it as it is, and do not ask yes or no first. Otherwise ask whether payers should also be able to pay with USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:

    Go to https://lightning-swap.com, sign in for API keys, create a key, and copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste it here and I will store it — or say “Bitcoin only” and I will continue without it.

    Mention FixedFloat only if they already use it.

  4. When they paste it. If it does not start with lightning+swapconnect://, ask them to copy it again. Otherwise add LSC_URI_PRIMARY=<paste> to the same .env with the file-editing tool, never a shell command. Swaps are now on, so build the refund route back (the swap non-negotiable below) as part of this integration. If they chose Bitcoin only, leave LSC_URI_PRIMARY unset and skip that route.
  5. Make the server load the file — yourself. OpenReceive reads os.environ, and a .env file on disk is not in it. When the app is started with Docker Compose, give the service env_file: .env. flask run loads .env when python-dotenv is installed (add python-dotenv to the requirements). Gunicorn does not: start it as dotenv -f .env run -- gunicorn … (python-dotenv’s CLI, pip install "python-dotenv[cli]"), in the app’s run script or Procfile, not just your own command. Restart the server after writing the file. Doctor reads .env from its working directory by itself. Never source, export or cat the file: the codes contain &, and the shell prints the pieces.

Do not invent placeholder URIs. Start the quickstart only once NWC_URI is saved and LSC_URI_PRIMARY is saved or explicitly declined. The first boot runs the receive-only preflight: if it reports spend methods such as pay_invoice, remove NWC_URI from .env and ask for a receive-only code again. Never set the spend-capable override to get past it.

After the Blueprint is registered, run openreceive doctor --app <module>:openreceive, naming the OpenReceiveFlask object (inside the app’s container when it runs under Compose). It reports NWC_URI presence without printing it, probes the wallet, and names any hook still on a placeholder and whether the migration ran. Exit code 1 is a finding to fix, not a reason to skip the check.

Upgrading an existing install

This is not the opening move of a new integration. When OpenReceive is ALREADY installed here, the doctor above is the first check. Compare the installed openreceive (pip show openreceive) against the release named at the top of this file, and the copied openreceive_flask.py against the quickstart’s. Upgrade first.

Only then start the quickstart.

After the quickstart — hand over, then stop

The quickstart is done when openreceive doctor --app <module>:openreceive --url <app url> is clean and this app serves the checkout page for one of its orders. Doctor names any failed check and exits nonzero; fix it before going on.

Give the user that checkout link. The browser check in the quickstart’s step 6 (payment-method icons, wallet logos, a pay tutorial) is theirs: name it in one line of your closing message, and do not run it yourself.

You cannot pay the invoice: the code is receive-only. Do not pay, settle or mark an order paid, and do not look for a way to (a wallet control port, a test endpoint, another wallet). If the user wants a real settlement test, they pay on that link from their own wallet, and on_paid marks the order paid.

Setup ends here. Your last message starts “Setup is finished” and has at most five short lines (about 80 characters each): the link, the methods it offers, and one line of what to look at. Send nothing after it. Do not list what changed, copy out the quickstart’s browser checklist, offer more work, or end the message on a question.

Non-negotiables

The quickstart below has the code. These are the rules it cannot state for itself, and they hold for every integration.

Your tables, not ours

openreceive scaffold payments --alembic (Flask-Migrate is Alembic), --sql, or the quickstart’s start-up helper creates openreceive_payments and openreceive_meta in THIS application’s database. That is the whole persistence OpenReceive needs. It does not replace your orders, users, or products, and you do not join them.

If you build your own checkout UI

The drop-ins (<Checkout>, <openreceive-checkout>) already obey all of this. This list is the short form of https://openreceive.org/guides/checkout-ux.md, for a UI built on @openreceive/browser/headless. Read that before writing components.

More documentation

Fetch one when the moment comes. Each is raw markdown, so a plain GET is enough; drop the .md for the same page a person would read.

Questions, or a problem with the library itself: https://openreceive.org/contact


The quickstart, in full

Inlined verbatim so this file needs no network access — follow it once Step 0 passes. The page it comes from is https://openreceive.org/guides/quickstart-flask.

Flask quickstart

Flask with Jinja templates. Requires Python ≥ 3.10, Flask ≥ 3 and a SQLAlchemy 2 Engine for OpenReceive’s two tables.

OpenReceive ships no openreceive.flask package, on purpose. The engine’s HTTP handler is framework-free, and a Flask Blueprint over it is the one file below, which you copy into your app. It becomes a package (openreceive[flask]) once two things are true: someone outside this repository has used it, and its code has gone a release without changes. If you copy it and hit a rough edge, that report is what moves it.

1. Install

pip install "openreceive[sqlalchemy]"

OpenReceive is one Python distribution with framework extras. openreceive[sqlalchemy] brings SQLAlchemy 2. The base package includes the wallet client (websockets, coincurve, cryptography), the HTTP engine, the openreceive CLI and the checkout’s script and stylesheet, so a Flask app with Jinja templates needs no npm. Add your database driver if the app does not have one yet, such as psycopg[binary] for Postgres.

Use uv or a virtualenv on Python 3.10 or newer. A system Python 3.9 cannot install the package.

2. Create the payment tables

OpenReceive needs two tables in your database:

With Flask-Migrate (it is Alembic), write a revision and apply it the way you apply your own:

openreceive scaffold payments --alembic --dialect postgres --out-dir migrations/versions   # or sqlite | mysql

Set the revision’s down_revision to your current head (flask db heads), then run flask db upgrade. The command never opens a database connection. → openreceive scaffold payments

An app that creates its own tables at start with db.create_all() creates these two beside them. Their DDL has no IF NOT EXISTS, so the table’s presence is the marker:

from sqlalchemy import inspect, text
from openreceive.storage.sql import payments_ddl_statements


def create_payment_tables(engine):
    if not inspect(engine).has_table("openreceive_payments"):
        with engine.begin() as connection:
            for statement in payments_ddl_statements(engine.dialect.name):
                connection.execute(text(statement))

Run it once per start, in one process, as db.create_all() runs. Anything else that runs your migrations can take openreceive scaffold payments --sql --dialect postgres, which prints the same DDL. OpenReceive owns the tables’ logic at runtime: locking, write-once settlement, and the reconciliation state machine. Details: Payment storage.

3. Add wallet credentials

Put the credentials in the server’s environment. That can be a .env your process manager loads, Compose’s env_file, or your secret store:

NWC_URI=
LSC_URI_PRIMARY=
LSC_URI_BACKUP=
  1. Get a receive-only NWC code from a compatible wallet (get one here). Put it in NWC_URI.
  2. Optional: set up a swap provider. Put its connection string in LSC_URI_PRIMARY, and a second one in LSC_URI_BACKUP if you have one.

Never put these values in browser code. Your app refuses to start if the NWC code also advertises spend methods such as pay_invoice. Create a receive-only code instead (Security).

OpenReceive reads os.environ, so a .env file on disk is not enough on its own. flask run loads .env when python-dotenv is installed. Gunicorn does not: under Docker Compose give the service env_file: .env, and elsewhere let the process manager or secret store inject the values. Environment variables lists every variable.

4. Wire OpenReceive

Copy this file into your app as openreceive_flask.py. It is the whole integration: a Blueprint whose one route hands each request to the engine, and an object that builds the engine once.

# openreceive_flask.py: OpenReceive for Flask. Copy this file into your app.
"""The OpenReceive routes as a Flask Blueprint over the framework-free engine.

`host` and `repository` are what `openreceive doctor --app app:openreceive`
reads, without touching the wallet. `start()` builds the stack once; building
the Service is the receive-only wallet check, so call it where the app boots.
"""

import os
import threading

from flask import Blueprint, Response, request

from openreceive.nwc.receive_client import NwcReceiveClient
from openreceive.server import (
    ConfigurationError,
    HttpRequest,
    HttpResponse,
    OpenReceiveApp,
    Service,
)
from openreceive.server.service import redact_secrets
from openreceive.storage.sql import SqlPaymentRepository

METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]


class OpenReceiveFlask:
    def __init__(self, host, engine, *, rate_limiting=False):
        self.host = host
        # OpenReceive's own repository over your Engine: its two tables,
        # the per-reference lock, write-once settlement and the reconcile gate.
        self.repository = SqlPaymentRepository(engine)
        self.rate_limiting = rate_limiting
        self._app = None
        self._lock = threading.Lock()

    def start(self):
        """Build the stack once. Raises ConfigurationError when NWC_URI is
        missing, unreachable or able to spend."""
        with self._lock:
            if self._app is None:
                nwc = os.environ.get("NWC_URI", "").strip()
                if not nwc:
                    raise ConfigurationError(
                        "NWC_URI is not set. Get a receive-only code: "
                        "https://openreceive.org/get_a_nwc_code_to_receive_payments"
                    )
                self._app = OpenReceiveApp(
                    # Service reads LSC_URI_* for swaps and refuses a code that can spend.
                    service=Service(NwcReceiveClient(nwc)),
                    host=self.host,
                    repository=self.repository,
                    prefix="",
                    rate_limiting=self.rate_limiting,
                )
            return self._app

    @property
    def app(self):
        return self.start()

    def blueprint(self, url_prefix="/openreceive"):
        bp = Blueprint("openreceive", __name__, url_prefix=url_prefix)

        @bp.route("/<path:rest>", methods=METHODS)
        def dispatch(rest):
            try:
                app = self.app
            except ConfigurationError as error:
                response = HttpResponse(
                    503,
                    {
                        "code": "WALLET_UNAVAILABLE",
                        "message": f"OpenReceive is not configured: {redact_secrets(str(error))}",
                        "retryable": True,
                    },
                    {},
                )
            else:
                bodyless = request.method in ("GET", "HEAD", "OPTIONS")
                response = app.handle(
                    HttpRequest(
                        method=request.method,
                        path=f"/{rest}",
                        query_string=request.query_string.decode(),
                        headers=dict(request.headers),
                        # A reader: the engine caps the body before it reads it.
                        body=None if bodyless else request.stream.read,
                        content_length=request.content_length,
                        remote_addr=request.remote_addr,
                        # What `authorize` gets as `context.request`.
                        framework_request=request,
                    )
                )
            return Response(
                response.json(), response.status, response.headers, mimetype="application/json"
            )

        return bp

Then wire your three hooks where the app is built. This example uses Flask-SQLAlchemy, an Order model and the buyer’s id in Flask’s session; map it onto the models and the login this app already has:

import os
from flask import Flask, session
from sqlalchemy import create_engine, text
from openreceive.server import Host
from openreceive_flask import OpenReceiveFlask
from .models import Order, db  # your existing models


def find_order(reference):
    reference = str(reference or "")
    return db.session.get(Order, int(reference)) if reference.isdigit() else None


host = Host(
    # The price for a reference — here, your order id — from your own data;
    # OpenReceive converts it into the Lightning invoice. Return None when
    # there is nothing to pay for. `value` is a decimal STRING from the order
    # row, never a float and never a request param. `description` is what the
    # payer is buying, in your own words.
    amount_for=lambda reference: (
        {"currency": "USD", "value": str(order.total), "description": order.summary}
        if (order := find_order(reference)) is not None and order.status == "awaiting_payment"
        else None
    ),
    # Your own access check: may this caller pay for this order? It runs inside
    # the request, so `session` and Flask-Login's `current_user` work here.
    # `context.resource["reference"]` is a claim the payer's browser sent, not proof.
    authorize=lambda context: (
        (order := find_order(context.resource.get("reference"))) is not None
        and order.user_id == session.get("user_id")
    ),
    # INSIDE the settlement transaction; runs only for the reference's first
    # settled attempt. Write through `settlement.connection` (that transaction),
    # never db.session. The WHERE clause is the lock.
    on_paid=lambda settlement: settlement.connection.execute(
        text("UPDATE orders SET status = 'paid' WHERE id = :id AND status = 'awaiting_payment'"),
        {"id": int(settlement.reference)},
    ),
)

# OpenReceive's own Engine on the SAME database as your orders. On SQLite it
# must not be Flask-SQLAlchemy's db.engine: the repository sets it up for one
# writer at a time.
payments_engine = create_engine(os.environ["DATABASE_URL"])

# `rate_limiting=True` caps invoice creation at 60 per client IP per hour, for
# public web shops. Leave it off for point-of-sale, where payers share an IP.
openreceive = OpenReceiveFlask(host, payments_engine, rate_limiting=True)
app.register_blueprint(openreceive.blueprint())  # serves /openreceive/...

# The receive-only wallet check, where the app boots: a missing, unreachable
# or spend-capable code stops the server here instead of failing the first payer.
openreceive.start()

openreceive.start() runs once per process. A command that imports the app with no wallet access, such as a build step, can skip it: the first payment request then runs the check, and the routes answer 503 WALLET_UNAVAILABLE until it passes. There is no background reconciler, meaning no job that checks pending payments on a timer. Settlement runs during normal requests instead, through the durable reconcile gate.

Flask-WTF’s CSRF protection stays on. CSRFProtect checks every POST, the payment routes included, and the checkout sends the page’s token in a header (step 5). Do not exempt the Blueprint. An app without Flask-WTF needs nothing extra: every mounted route refuses a request whose Sec-Fetch-Site header says cross-site, and authorize is the real boundary (Security).

Behind a reverse proxy, wrap the app in Werkzeug’s ProxyFix (app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)). Otherwise request.remote_addr is the proxy, and rate_limiting counts it as the one payer. → Rate limiting

An optional worker, openreceive notifications --app app:openreceive, listens for wallet payment notifications so settlement does not wait for the next page load. → openreceive notifications

Your app also needs an ordinary order-creation route that validates the cart, prices with exact decimal math, and returns the order id the page will pass as the reference. OpenReceive never prices from payer input.

The reference is a string you choose, and it is the fulfillment identity: your order id. Use one per thing you fulfill, create it before checkout, keep it across retries, and never reuse it. OpenReceive never looks inside it, but the reference still matters:

The Python API is snake_case (payment_hash, amount_msats), and so is everything on the wire.

5. Render checkout

The checkout’s script and stylesheet ship inside the openreceive package. Serve them from the installed copy, so an upgrade never leaves a stale file:

from importlib.resources import files
from flask import send_from_directory

CHECKOUT_ASSETS = str(files("openreceive") / "django" / "static" / "openreceive")


@app.get("/checkout-assets/<path:name>")
def checkout_assets(name):
    return send_from_directory(CHECKOUT_ASSETS, name)

Then render the checkout on the order’s own page:

<head>
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <link rel="stylesheet" href="/checkout-assets/openreceive-checkout.css">
  <script type="module" src="/checkout-assets/openreceive-checkout.js"></script>
</head>

<openreceive-checkout reference="{{ order.id }}" prefix="/openreceive"
                      csrf-header="X-CSRFToken"></openreceive-checkout>

The checkout renders, polls, and settles itself. csrf-header sends the <meta name="csrf-token"> value in Flask-WTF’s X-CSRFToken header on every request it makes. Without Flask-WTF, drop the meta tag and the attribute. The checkout already shows the description from amount_for and the collapsed transaction-details panel; do not build a custom UI to show them (Checkout UX).

The order page is the payer’s way back: a payer can reload or bookmark it to return to a pending payment or a swap refund. Set the theme attribute (dark or light) when the page always uses one theme. The stylesheet is scoped to the checkout and needs no Tailwind processing; your page’s styles can still override it (Frontend checkout). Everything the checkout draws ships inside the script, so there is no image to serve. If your Content-Security-Policy has a strict img-src, allow data:. An app with a JavaScript build can use @openreceive/react, /vue, /svelte or /angular from npm instead, with csrfHeader="X-CSRFToken".

That is the whole loop. Your server owns the price and the order. The payer gets an invoice. on_paid runs inside the settlement transaction. If that transaction rolls back, the callback may run again. For delivery to outside systems, use an outbox in your app: record the message in the transaction and send it after commit, or use after_paid on the Host.

examples/flask-starter is this page as a one-product shop. It is not a template to copy models from; map its shape onto the models in THIS app.

6. Verify

openreceive doctor --app app:openreceive --url http://localhost:5000

--app module:attr names the OpenReceiveFlask object. Doctor reads .env in the working directory for variables the environment does not set, and never prints a value. When the app runs under Docker Compose, run doctor inside the service (docker compose exec web openreceive doctor …), where env_file has set the environment.

openreceive doctor checks Python, NWC_URI, and swap-provider configuration. It probes the wallet relay to confirm the code is receive-only, confirms the migration ran, and names any hook still on a placeholder. --url confirms the routes are mounted. Every failing line states its own fix, and the command exits with code 1 when anything fails. openreceive debug-report prints the same output as a redacted support report. → openreceive doctor

Then open the checkout in a browser, confirm the payment-method icons and wallet logos render, and open a wallet’s pay tutorial to check its screenshots. If an image is missing, inspect the console for CSP violations and the Network panel for failed script loads. Allow data: in img-src. Do not add image routes, copy package source images, or use registry icon_path / tutorial path keys as browser URLs.

Swap credentials (LSC_URI_*) stay server-side too. The provider order id and token live in the attempt’s server-only swap_data column. They never reach a response or a log (Automated swaps).