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

OpenReceive agent directions (Astro)

# READ THIS FIRST: this file is 35 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/astro/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 an Astro site — the one you are already working in, on @astrojs/node. OpenReceive ships no Astro package: the integration is one endpoint, rendered on demand, that hands each request to @openreceive/http. You do not need a copy of the OpenReceive source: the packages are on npm, and the Astro 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 catch-all endpoint (src/pages/openreceive/[...path].ts, rendered on demand on @astrojs/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, and a .env file on disk is not in it. astro dev reads .env only for the variables in the app’s env.schema, so start it with node --env-file=.env node_modules/.bin/astro dev, and start the built server with node --env-file=.env ./dist/server/entry.mjs. When the app is started with Docker Compose, give the service env_file: .env. Never declare a code with context: "client", and never give it a PUBLIC_ prefix: either sends it to the browser. 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/astro-recipe.

Astro recipe

OpenReceive ships no Astro package. An Astro endpoint takes a Web-standard Request and returns 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 astro 7.3, @astrojs/node 11.1 (standalone) and @openreceive/* 0.4.25. The built app ran on Node in Docker, 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.

npx astro add node
npm install @openreceive/http @openreceive/elements pg

Where it runs

On Node, with @astrojs/node. The payment route and the checkout page render on demand (prerender = false); the rest of the site can stay static. The wallet connection and your database driver need Node, so the route cannot run on an edge runtime. The Vercel and Netlify adapters deploy Node functions: 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.

Secrets

Declare them in astro.config.mjs. A secret is read when the server runs, never written into the build:

// astro.config.mjs
import { defineConfig, envField } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  adapter: node({ mode: "standalone" }),
  env: {
    schema: {
      NWC_URI: envField.string({ context: "server", access: "secret" }),
      DATABASE_URL: envField.string({ context: "server", access: "secret" }),
    },
  },
});

The payment route

// src/lib/openreceive.ts
import { createStack, type Stack } from "@openreceive/http";
import type { APIContext } from "astro";
import { NWC_URI } from "astro:env/server";
import { db } from "./db"; // your existing pg Pool
import { findOrder } from "./orders"; // 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: 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 ({ native, resource }) => {
      const context = native as APIContext;
      const order = resource.reference ? await findOrder(resource.reference) : undefined;
      return Boolean(order && order.visitor === context.cookies.get("visitor")?.value);
    },
    // The client address the adapter reports.
    rateLimiting: {
      ip: ({ native }) => (native as APIContext).clientAddress,
    },
  });
  return stack;
}

export function handleOpenReceive(context: APIContext): Promise<Response> {
  return openReceive().handler(context.request, { native: context });
}
// src/pages/openreceive/[...path].ts — every route under /openreceive/
import type { APIRoute } from "astro";
import { handleOpenReceive } from "../../lib/openreceive";

export const prerender = false;

export const GET: APIRoute = (context) => handleOpenReceive(context);
export const POST: APIRoute = (context) => handleOpenReceive(context);

native is the endpoint’s APIContext, so authorize can check the session your app already has: its cookies, or the user your middleware puts in context.locals. The Next.js quickstart explains each hook. They are the same here.

One stack serves the whole server process. Astro’s origin check refuses cross-site form posts. The checkout sends JSON, which it lets through. clientAddress is the socket address. Behind your own reverse proxy, list your domain in security.allowedDomains: Astro then takes the payer’s address from x-forwarded-for (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

The page needs no UI framework: it uses the <openreceive-checkout> element. With React, Vue or Svelte in your Astro app, the @openreceive/react, vue or svelte component works as a client:load island instead.

---
// src/pages/checkout/[reference].astro
import "@openreceive/elements/styles.css";
import { findOrder } from "../../lib/orders";

export const prerender = false;

const order = await findOrder(Astro.params.reference ?? "");
if (!order || order.visitor !== Astro.cookies.get("visitor")?.value) {
  return new Response(null, { status: 404 });
}
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Checkout</title>
  </head>
  <body>
    <openreceive-checkout reference={order.id} prefix="/openreceive"></openreceive-checkout>
    <script>
      import { defineElements } from "@openreceive/elements";
      defineElements();
    </script>
  </body>
</html>

The element 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

In production, set them all in the server’s environment. In development, astro dev reads .env for the variables in your schema but keeps the file out of process.env. To try swaps locally, start it with node --env-file=.env node_modules/.bin/astro dev. Never declare them with context: "client", which would send them to the browser.

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.