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

OpenReceive agent directions (React Router)

# READ THIS FIRST: this file is 34 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/react-router/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.26.

Add OpenReceive to a React Router application in framework mode — the app you are already working in, on Node. OpenReceive ships no React Router package: the integration is one resource route that hands each request to @openreceive/http. You do not need a copy of the OpenReceive source: the packages are on npm, and the React Router recipe is appended to this file in full as the quickstart, so you can do the whole integration without fetching anything. Prefer the published packages and the handler they export — do not reimplement wallet RPC, settlement, or pricing.

Do not clone the OpenReceive repository into this app, and do not copy a demo’s models (ShopOrder, a signed-cookie visitor, an in-memory 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 frontend framework, authentication and database. Pick the UI package that matches the frontend that is already here (@openreceive/react, /vue, /svelte, /angular, or /elements for plain HTML) — do not add React to a Vue app. Reuse the app’s existing session/auth in authorize and its existing database handle in storage.db.

What OpenReceive is

A payment library that runs inside YOUR server. It mounts as ONE resource route (app/routes/openreceive.ts, registered as openreceive/* in app/routes.ts, on Node) in 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, amountFor, onPaid; 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.

On a hosted builder (v0, Vercel, Replit, Lovable), the user may say instead that both codes are already set as the project’s environment variables or secrets. Believe them: do not ask for the codes, do not write .env, and do not read or print the variables. Skip step 5’s file loading: the platform already puts them in process.env. Do not add them to a compose file or any other deploy config either: they already reach the app, and a line such as NWC_URI: ${NWC_URI} replaces them with an empty value.

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, Vercel or 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 process.env. react-router dev loads .env into it, but react-router-serve does not: start the built server with node --env-file=.env node_modules/.bin/react-router-serve ./build/server/index.js. When the app is started with Docker Compose, give the service env_file: .env. Never give a code a VITE_ prefix: that puts it in the browser bundle. Restart the server after writing the file. Under Compose, recreate the container with docker compose up -d: docker compose restart keeps its old environment.

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.

Upgrading an existing install

This is not the opening move of a new integration. When OpenReceive is ALREADY installed here, npx openreceive doctor reports every credential as set/unset (never the value) and probes the wallet. Check the installed @openreceive/node and @openreceive/browser against the release named at the top of this file: the headless display models below do not exist in older versions, and the first tile click throws with nothing saying why. Upgrade first.

Only then start the quickstart.

After the quickstart — hand over, then stop

The quickstart is done when npx openreceive doctor --db <file-or-url> --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. Doctor reads .env itself. Never source it into a shell: the & in a code splits the value and prints the pieces.

Give the user that checkout link. The browser check of that page (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 onPaid 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

npx openreceive scaffold payments emits openreceive_payments and openreceive_meta for 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/react-router-recipe.

React Router recipe

OpenReceive ships no React Router package. In React Router’s framework mode (formerly Remix), a resource route’s loader and action take a Web-standard Request and can return a Response, and so does @openreceive/http, so the integration is the route below. Your server code creates each Lightning invoice, the payment goes straight to your wallet, and payment attempts are stored in your app’s database. There is no OpenReceive account and no background worker.

Optional swaps let customers pay with USDT, USDC, SOL, and ETH. A swap provider you configure converts the payment to BTC over Lightning, and it settles into the same wallet. Available assets and networks depend on the provider.

This recipe was checked with react-router 8.4 (the default template, with Tailwind) and @openreceive/* 0.4.25. The built app ran on Node in Docker (react-router-serve), with Postgres and a test wallet on a public NWC relay. In Chromium, the buyer’s checkout page showed the Lightning invoice. A stranger got 403 from the payment routes and 404 from the page, and a request that named its own amount got 400. When the wallet marked the invoice paid, the page showed it paid and onPaid marked the order paid.

npm install @openreceive/http @openreceive/react pg

Where it runs

On Node, with react-router-serve or your own Express server. The wallet connection and your database driver need Node, so the route cannot run on an edge runtime. On Vercel or Netlify, read Bitcoin checkout on serverless hosts first. For Cloudflare Workers, the TanStack Start recipe explains what a Worker needs. This recipe was not run on those hosts.

The payment route

Keep the handler in a .server.ts module. React Router refuses to bundle one into browser code:

// app/lib/openreceive.server.ts
import { createStack, type Stack } from "@openreceive/http";
import { db } from "./db.server"; // your existing pg Pool
import { currentVisitor, findOrder } from "./orders.server"; // your own orders

let stack: Stack | undefined;

// Built on the first request, not at import, so the build never connects
// to the wallet.
function openReceive(): Stack {
  stack ??= createStack({
    wallet: { nwc: process.env.NWC_URI ?? "" },
    storage: {
      db,
      // Runs once, inside the transaction that records the payment.
      onPaid: async ({ reference, paidAt, query }) => {
        await query(
          "UPDATE orders SET state = 'paid', paid_at = $1 WHERE id = $2 AND state = 'awaiting_payment'",
          [paidAt, reference],
        );
      },
    },
    // The price comes from the order row, never from the request.
    amountFor: async (reference) => {
      const order = await findOrder(reference);
      return order?.state === "awaiting_payment"
        ? { currency: order.currency, value: order.amount, description: order.title }
        : null;
    },
    // Only the buyer may pay for their order.
    authorize: async ({ request, resource }) => {
      const order = resource.reference ? await findOrder(resource.reference) : undefined;
      const visitor = await currentVisitor(request);
      return Boolean(order && visitor && order.visitor === visitor);
    },
    // A loader sees no socket address. Read the header your reverse proxy
    // sets and overwrites; never one a client can send through it.
    rateLimiting: {
      ip: ({ request }) => request.headers.get("x-real-ip") ?? undefined,
    },
  });
  return stack;
}

export function handleOpenReceive(request: Request): Promise<Response> {
  return openReceive().handler(request);
}
// app/routes/openreceive.ts
import { handleOpenReceive } from "~/lib/openreceive.server";
import type { Route } from "./+types/openreceive";

// A resource route: no component, so React Router returns these Responses as-is.
export const loader = ({ request }: Route.LoaderArgs) => handleOpenReceive(request);
export const action = ({ request }: Route.ActionArgs) => handleOpenReceive(request);
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("openreceive/*", "routes/openreceive.ts"), // every route under /openreceive/
  route("checkout/:reference", "routes/checkout.tsx"),
] satisfies RouteConfig;

authorize sees the incoming request with its cookies, so check it with the session your app already has. With createCookieSessionStorage, currentVisitor is (await getSession(request.headers.get("cookie"))).get("visitor"). The Next.js quickstart explains each hook. They are the same here.

One stack serves the whole server process. A loader cannot see the connection, so the client address comes from a header. Without a proxy that sets it, the limit does not apply (Rate limiting).

Your database

db is a pg Pool, node:sqlite or better-sqlite3, or one of the adapters in Node ORM recipes. With Drizzle, pass the pg Pool underneath it.

Create OpenReceive’s two tables with your migrations (npx openreceive scaffold payments --orm <your orm>), or run paymentsSchemaSql("postgres") from @openreceive/http once. It is idempotent. See Payment storage.

The checkout page

// app/routes/checkout.tsx
import { Checkout } from "@openreceive/react";
import "@openreceive/react/styles.css";
import { data } from "react-router";
import { currentVisitor, findOrder } from "~/lib/orders.server";
import type { Route } from "./+types/checkout";

export async function loader({ params, request }: Route.LoaderArgs) {
  const order = await findOrder(params.reference);
  if (!order || order.visitor !== (await currentVisitor(request))) {
    throw data("Not found", { status: 404 });
  }
  return { reference: order.id };
}

export default function OrderCheckout({ loaderData }: Route.ComponentProps) {
  return <Checkout reference={loaderData.reference} prefix="/openreceive" />;
}

Import styles.css from the route module, as above. Do not @import it into your Tailwind entry (app/app.css); the checkout styles are compiled already. The page renders on the server and starts the checkout in the browser. The payer picks Bitcoin and gets the invoice. Keep it at the order’s own URL: a payer with a swap deposit in flight comes back to it (Swap refunds).

Settings

The route reads them from process.env when the first request arrives. react-router dev loads .env into process.env. In production, set them in the server’s environment: react-router-serve does not read .env. Never give them a VITE_ prefix, which would put them in the browser bundle.

No worker or cron job is needed. Each request to the payment routes also checks the wallet for settled invoices, through a lock in your database. A payer who closes the tab is settled on the next request.