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

OpenReceive agent directions (Nuxt)

# READ THIS FIRST: this file is 33 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/nuxt/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 Nuxt application — the app you are already working in, on Nitro’s node-server preset. OpenReceive ships no Nuxt package: the integration is one server 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 Nuxt 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 server route (server/routes/openreceive/[...path].ts, on Nitro’s node-server preset) 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. nuxt dev loads .env into it, but the built server does not: start it with node --env-file=.env .output/server/index.mjs. When the app is started with Docker Compose, give the service env_file: .env. Never copy a code into runtimeConfig.public or a NUXT_PUBLIC_ variable: both go 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/nuxt-recipe.

Nuxt recipe

OpenReceive ships no Nuxt package. A Nuxt server route can hand the request to @openreceive/http as a Web-standard Request and send back the Response it returns, 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 nuxt 4.6 (Nitro 2, h3 1.15) and @openreceive/* 0.4.25. The built app ran on Node in Docker (node .output/server/index.mjs), 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/vue pg

Where it runs

On Node, with Nitro’s default node-server preset. The wallet connection and your database driver need Node, so the route cannot run on an edge runtime. The Vercel, Netlify and AWS Lambda presets 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.

The payment route

Files in server/ run only on the server, and Nuxt auto-imports the h3 helpers and everything in server/utils/ there:

// server/utils/openreceive.ts
import { createStack, type Stack } from "@openreceive/http";
import type { H3Event } from "h3";

let stack: Stack | undefined;

// Built on the first request, not at import, so the build and prerendering
// never connect to the wallet.
function openReceive(): Stack {
  stack ??= createStack({
    wallet: { nwc: process.env.NWC_URI ?? "" },
    storage: {
      db, // your existing pg Pool, from server/utils/db.ts
      // 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); // your own orders
      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 event = native as H3Event;
      const order = resource.reference ? await findOrder(resource.reference) : undefined;
      return Boolean(order && order.visitor === getCookie(event, "visitor"));
    },
    // The socket address. Behind your own proxy, pass { xForwardedFor: true }.
    rateLimiting: {
      ip: ({ native }) => getRequestIP(native as H3Event),
    },
  });
  return stack;
}

export function handleOpenReceive(event: H3Event): Promise<Response> {
  return openReceive().handler(toWebRequest(event), { native: event });
}
// server/routes/openreceive/[...path].ts — every route under /openreceive/
export default defineEventHandler((event) => handleOpenReceive(event));

Put it in server/routes/, not server/api/, which would add /api to the path. native is the request’s H3Event, so authorize can check the session your app already has: its cookies, or the user your session module reads from the event. The Next.js quickstart explains each hook. They are the same here.

One stack serves the whole server process. getRequestIP returns the socket address. Behind your own reverse proxy, { xForwardedFor: true } reads the first address in x-forwarded-for, which is safe only when that proxy sets the header (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 asks your own server route whether this visitor owns the order:

// server/api/checkout/[reference].get.ts
export default defineEventHandler(async (event) => {
  const order = await findOrder(getRouterParam(event, "reference") ?? "");
  if (!order || order.visitor !== getCookie(event, "visitor")) {
    throw createError({ statusCode: 404, statusMessage: "Not found" });
  }
  return { reference: order.id };
});
<!-- app/pages/checkout/[reference].vue -->
<script setup lang="ts">
import Checkout from "@openreceive/vue/checkout.vue";
import "@openreceive/vue/styles.css";

const route = useRoute();
// During server rendering, useFetch passes the visitor's cookies to your own routes.
const { data, error } = await useFetch(`/api/checkout/${route.params.reference}`);
if (error.value) {
  throw createError({ statusCode: 404, statusMessage: "Not found", fatal: true });
}
</script>

<template>
  <Checkout v-if="data" :reference="data.reference" prefix="/openreceive" />
</template>

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. nuxt dev loads .env into process.env. In production, set them in the server’s environment: the built server does not read .env. Do not copy them into runtimeConfig.public, which goes 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.