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

OpenReceive agent directions (SvelteKit)

# READ THIS FIRST: this file is 35 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/sveltekit/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 SvelteKit application — the app you are already working in, on @sveltejs/adapter-node. OpenReceive ships no SvelteKit package: the integration is one endpoint that hands each request to @openreceive/http. You do not need a copy of the OpenReceive source: the packages are on npm, and the SvelteKit 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/routes/openreceive/[...path]/+server.ts, on Node with adapter-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. vite dev reads .env only for the variables the app declares, so start it with node --env-file=.env node_modules/.bin/vite dev, and start the built server with node --env-file=.env build. When the app is started with Docker Compose, give the service env_file: .env. Never make a code public (public: true in src/env.ts, or a PUBLIC_ prefix): that 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/sveltekit-recipe.

SvelteKit recipe

OpenReceive ships no SvelteKit package. A SvelteKit endpoint (+server.ts) 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 @sveltejs/kit 3.0, @sveltejs/adapter-node 6.0 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.

npm install @openreceive/http @openreceive/svelte pg

Where it runs

On Node, with @sveltejs/adapter-node. 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

SvelteKit 3 reads the environment variables you declare in src/env.ts. The build runs your app to analyse it, without your secrets, so require them only when the app starts:

// src/env.ts
import { building } from "$app/env";
import { defineEnvVars } from "@sveltejs/kit/env";

const secret = (value: string | undefined): string => {
  if (value) return value;
  if (building) return "";
  throw new Error("is not set");
};

export const variables = defineEnvVars({
  NWC_URI: { schema: secret, description: "Receive-only NWC code from your wallet" },
  DATABASE_URL: { schema: secret, description: "Postgres connection string" },
});

Never mark them public: true, which would send them to the browser.

The payment route

Keep the handler under src/lib/server/. SvelteKit refuses to bundle a module from there into browser code:

// src/lib/server/openreceive.ts
import { createStack, type Stack } from "@openreceive/http";
import type { RequestEvent } from "@sveltejs/kit";
import { NWC_URI } from "$app/env/private";
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: the build imports server
// modules to analyse them, and a stack connects to the wallet at once.
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 event = native as RequestEvent;
      const order = resource.reference ? await findOrder(resource.reference) : undefined;
      return Boolean(order && order.visitor === event.cookies.get("visitor"));
    },
    // adapter-node's client address: the socket, or ADDRESS_HEADER behind a proxy.
    rateLimiting: {
      ip: ({ native }) => (native as RequestEvent).getClientAddress(),
    },
  });
  return stack;
}

export function handleOpenReceive(event: RequestEvent): Promise<Response> {
  return openReceive().handler(event.request, { native: event });
}
// src/routes/openreceive/[...path]/+server.ts — every route under /openreceive/
import { handleOpenReceive } from "#lib/server/openreceive.ts";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = (event) => handleOpenReceive(event);
export const POST: RequestHandler = (event) => handleOpenReceive(event);

native is the request’s RequestEvent, so authorize can check the session your app already has: its cookies, or the event.locals.user your hooks.server.ts sets. The Next.js quickstart explains each hook. They are the same here.

One stack serves the whole server process. Behind a reverse proxy, set adapter-node’s ADDRESS_HEADER (and XFF_DEPTH), so that getClientAddress() returns the payer’s address and not the proxy’s (Rate limiting).

On SvelteKit 2, import the secret as env.NWC_URI from $env/dynamic/private, and import server modules from $lib/server/, with no src/env.ts. The rest is the same. This recipe was checked on SvelteKit 3 only.

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 driver underneath it. sv add drizzle sets up Postgres through postgres.js or Neon, which OpenReceive does not take directly. Then give OpenReceive its own small pg Pool on the same DATABASE_URL. It is the same database, not a second one.

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

The checkout page

// src/routes/checkout/[reference]/+page.server.ts
import { error } from "@sveltejs/kit";
import { findOrder } from "#lib/server/orders.ts";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ params, cookies }) => {
  const order = await findOrder(params.reference);
  if (!order || order.visitor !== cookies.get("visitor")) error(404);
  return { reference: order.id };
};
<!-- src/routes/checkout/[reference]/+page.svelte -->
<script lang="ts">
  import Checkout from "@openreceive/svelte/checkout.svelte";
  import "@openreceive/svelte/styles.css";
  import type { PageProps } from "./$types";

  let { data }: PageProps = $props();
</script>

<Checkout reference={data.reference} prefix="/openreceive" />

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

In production, set them all in the server’s environment. In development, vite dev reads .env for the variables in src/env.ts but keeps the file out of process.env. To try swaps locally, start it with node --env-file=.env node_modules/.bin/vite dev.

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.