This is the full file; follow it from Step 0.
OpenReceive agent directions (Medusa)
# READ THIS FIRST: this file is 21 KB and a summary drops required steps. Download it whole:
curl -fsSL https://openreceive.org/agent-directions/medusa/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):
- Before installing or editing anything, ask the user for a receive-only NWC code. One question per message.
- Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
- The user only pastes codes into this chat. Never ask them to edit .env or run a command to save one.
- Write each code into .env yourself, as Step 0 says. Never echo it or put it in a shell command.
- Do not suggest rotating or revoking a code because it was pasted here.
- Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said “Bitcoin only”.
These directions describe OpenReceive 0.4.28.
Add OpenReceive to the Medusa store you are working in: a Medusa server, and
usually a storefront in its own project (Medusa’s Next.js starter or another).
OpenReceive is a Medusa plugin on npm, @openreceive/medusa, with a payment
provider. The quickstart appended to this file has all the code: do not clone
the OpenReceive repository, and do not write your own payment provider, routes
or settlement.
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 buyers pay with USDT, USDC, ETH or SOL instead; the provider converts the payment to BTC over Lightning in the merchant’s connected wallet. Available assets and networks depend on the provider.
Medusa keeps carts, orders, prices and customers. The plugin keeps its payment attempts in two tables of Medusa’s database and places the order through Medusa’s own workflow once the payment settles. You write no hooks.
Step 0 — ask for the two codes, one question at a time
Before anything else, check one file: the Medusa server’s .env.
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, go to Step 1.
Otherwise your next action is a question to the user. Do not install packages,
edit the store or search anywhere else before asking it. Do not read deploy
config, Docker containers or process environments looking for a code, and
never run printenv: a new store has neither code yet.
The user never edits an environment file. They paste each code into this chat; you store it. That is the supported path: do not ask them to save it themselves, and do not tell them to revoke or replace a code because it was pasted here. Ask one question per message.
-
First message — the NWC code, and nothing else.
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.
- When they paste it. If it does not start with
nostr+walletconnect://, ask them to copy the receive-only code again. Otherwise writeNWC_URI=<paste>into the Medusa server’s.envwith your file-editing tool. Never write it with a shell command (echo,printf, a heredoc): the command line shows the code. Never put it in the storefront, in aNEXT_PUBLIC_variable or in a commit. Reply only that it is saved, then ask the next question. -
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: do not skip it, and do not ask yes or no first. Otherwise ask whether buyers 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.
- When they paste it. If it does not start with
lightning+swapconnect://, ask them to copy it again. Otherwise addLSC_URI_PRIMARY=<paste>to the same.envwith the file-editing tool. Swaps are now on, so keep the route back (the swap non-negotiable below).
Medusa loads .env itself. Under Docker Compose, give the Medusa service
env_file: .env and recreate it with docker compose up -d. Do not report
setup as complete until the NWC code is saved, and the LSC code is saved or
the user said “Bitcoin only”. Never invent a placeholder code.
Step 1 — install the plugin in the Medusa server
In the Medusa server’s project, run npm install @openreceive/medusa, then
register the plugin and its payment provider in medusa-config.ts exactly as
the quickstart’s “Install the plugin” shows, keeping every setting already
there. If the payment module is already configured, add the provider to its
providers list. Run npx medusa db:migrate, which creates the plugin’s two
tables, and restart Medusa. Medusa needs Node 22.12 or later for the plugin.
Restart Medusa and the storefront the way this project already runs them: in
their own terminal, under Docker Compose with docker compose up -d, or by
the pid of the process you started. Never pkill or killall by name: on a
shared machine that stops other people’s servers.
Step 2 — offer it in the regions and check the wallet
In the Medusa server’s project:
npx medusa exec ./node_modules/@openreceive/medusa/dist/enable-openreceive.js
npx openreceive doctor
The first adds OpenReceive to every region. doctor reads .env, reports
each code as set or unset, and asks the wallet whether it is receive-only. It
names any failed check and exits nonzero; fix it before going on. If it
reports spend methods such as pay_invoice, remove NWC_URI from .env and
ask the user for a receive-only code again. Never set the spend-capable
override to get past it.
Step 3 — add the checkout to the storefront
For Medusa’s Next.js starter, make the quickstart’s storefront changes:
npm install @openreceive/react, the route file
src/app/api/openreceive/[...path]/route.ts, the method in
src/lib/constants.tsx, and the OpenReceivePaymentButton in the payment
button. Keep the rest of the storefront as it is. Do not import
@openreceive/react/styles.css from a component or a CSS file: the starter’s
Tailwind build refuses it, which is why the route serves it.
For another storefront, follow the quickstart’s “Another storefront”: the
checkout at the step where the buyer confirms, with the OpenReceive payment
session’s id as reference, and the cart completed when it reports settled.
Restart the storefront. Then prove the plugin answers on the Medusa server:
curl -s -X POST <medusa url>/store/openreceive/checkouts \
-H 'content-type: application/json' \
-H 'x-publishable-api-key: <the storefront publishable key>' \
-d '{"reference":"payses_check"}'
The answer is OpenReceive’s JSON refusal, "code":"FORBIDDEN": no such
session exists, and the plugin’s routes are mounted. The publishable key is
public; it is the storefront’s NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY.
Step 4 — hand over, then stop
Setup is done when doctor is clean and the curl above answers FORBIDDEN.
The browser check is the user’s: name it in one line of your closing message,
and do not run it yourself. You cannot pay an 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).
Your last message starts “Setup is finished” and has at most five short lines: the storefront’s address, “add a product to the cart, check out, and pick Bitcoin & stablecoins at the payment step: the review step shows the checkout”, and the methods it offers (Bitcoin, plus USDT, USDC, ETH and SOL when the swap code is saved). Do not mention minimums, never say a coin will not work, do not list what you changed, and send nothing after it. Do not add containers, services, workers or cron jobs: the plugin’s job runs inside Medusa.
Non-negotiables
- Never print, log or commit a code, never put one in a shell argument, and
never write one into the storefront, source files or browser code.
doctor’s set/unset is all you report. - Do not suggest rotating, revoking or replacing a code because it was pasted into this chat; that is the supported path.
- Work only in this store’s projects. Never read or run anything from another
project or directory on this machine (its
node_modules, tools or source), for any reason. A browser, Playwright and reading the plugin’s source are not part of setup: whendoctor,db:migrateor the curl fails, report its output. - Receive-only NWC is required. Never get past the preflight with a code that can spend.
- The plugin owns only its two payment tables. Medusa owns carts, orders, prices, customers and email. Do not write your own payment provider, routes, subscribers or fulfillment code, and do not change Medusa’s own packages.
- IF SWAPS ARE ON, KEEP THE ROUTE BACK. A deposit that arrives short or late becomes refundable, and the buyer claims it later from the same checkout. Keep the plugin installed while swap payments may still need a refund. https://openreceive.org/guides/swap-refunds.md
- A receive-only wallet cannot send refunds. A refund made in the Medusa admin is recorded only; the merchant sends it from their own wallet.
Further reading
- Medusa Quickstart
- Automated Swaps
- Swap Refunds
- Lightning Swap Connect URI
- Security
- Price Feeds
- Payment Safety Upgrade
The quickstart, in full
Inlined verbatim so this file needs no network access. Steps 0–4 above are the setup and this is their reference: it has the code they name. Where the two differ, the steps win, and its admin screens are only for a store whose Medusa server has no shell. The page it comes from is https://openreceive.org/guides/quickstart-medusa.
Medusa quickstart
Medusa is an open-source commerce platform on Node. The OpenReceive plugin adds a payment provider to it: the buyer pays at checkout with Bitcoin over Lightning, straight into your own wallet, and the order is placed once the payment settles. With a swap provider, the same checkout also takes USDT, USDC, SOL and ETH, which the provider converts to BTC over Lightning into the same wallet.
Medusa keeps owning carts, orders, prices and customers. The plugin keeps its payment attempts in two tables of Medusa’s own database, over Medusa’s own connection. Each Medusa payment session is the reference of its payment. When a payment settles, Medusa’s own process-payment workflow captures the session and completes the cart into an order, whether or not the buyer is still on the page.
Requirements: Medusa 2 on Node 22.12 or later, and Medusa’s PostgreSQL database. Medusa must run its scheduled jobs, as a Medusa server does unless you split it into a server and a worker; then the worker runs them.
Install the plugin
In the Medusa server’s project:
npm install @openreceive/medusa
Register the plugin and its payment provider in medusa-config.ts, next to
what is already there:
module.exports = defineConfig({
projectConfig: {
// …your existing settings
},
plugins: [{ resolve: "@openreceive/medusa", options: {} }],
modules: [
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{ resolve: "@openreceive/medusa/providers/openreceive", id: "openreceive" },
],
},
},
],
})
If modules already lists the payment module with other providers, add this
provider to its providers list instead of a second entry. Then create the
plugin’s two tables and restart Medusa:
npx medusa db:migrate
The plugin mounts the checkout’s routes under /store/openreceive, where
Medusa checks the publishable API key as on every store route. It adds a job
that runs every minute, and Medusa’s id for the provider is
pp_openreceive_openreceive.
Configure the wallet
Put the codes in the Medusa server’s .env, never in the storefront:
NWC_URI=nostr+walletconnect://…
LSC_URI_PRIMARY=lightning+swapconnect://…
NWC_URI is a receive-only Nostr Wallet Connect code: it can create invoices
and read payments but cannot spend, and the plugin refuses a code that can
spend. LSC_URI_PRIMARY is optional; leave it out for Bitcoin-only checkout.
Medusa loads .env itself. Restart Medusa after changing it.
Then offer the method in your regions: in the admin, open Settings → Regions, edit each region and add Openreceive (OPENRECEIVE) to its payment providers. Or run, in the Medusa server’s project:
npx medusa exec ./node_modules/@openreceive/medusa/dist/enable-openreceive.js
It adds the provider to every region and says which ones it changed. Check the codes and the wallet with:
npx openreceive doctor
doctor reads .env, prints each code as set or unset (never its value) and
asks the wallet whether it is receive-only. It exits nonzero when a check
fails.
Add the checkout to the storefront
The buyer pays at the storefront’s review step, before the order is placed. For the Next.js starter, install the React checkout:
npm install @openreceive/react
Add src/app/api/openreceive/[...path]/route.ts. It forwards the checkout’s
calls to the Medusa server with the publishable key, as the starter’s own
server calls do, and serves the checkout’s stylesheet as it is, outside the
starter’s Tailwind build:
import { readFile } from "node:fs/promises"
import { createRequire } from "node:module"
import path from "node:path"
import { NextRequest } from "next/server"
const BACKEND_URL = process.env.MEDUSA_BACKEND_URL ?? "http://localhost:9000"
async function stylesheet() {
const resolve = createRequire(path.join(process.cwd(), "package.json")).resolve
const css = await readFile(resolve("@openreceive/react/styles.css"))
return new Response(css, {
headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=3600" },
})
}
async function forward(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path: segments } = await params
if (request.method === "GET" && segments.join("/") === "styles.css") return stylesheet()
const url = new URL(`/store/openreceive/${segments.join("/")}`, BACKEND_URL)
url.search = request.nextUrl.search
const response = await fetch(url, {
method: request.method,
headers: {
"content-type": request.headers.get("content-type") ?? "application/json",
accept: "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? "",
},
body: request.method === "GET" ? undefined : await request.text(),
cache: "no-store",
})
const headers = new Headers({ "cache-control": "no-store" })
for (const name of ["content-type", "retry-after", "x-request-id"]) {
const value = response.headers.get(name)
if (value) headers.set(name, value)
}
return new Response(await response.text(), { status: response.status, headers })
}
export { forward as GET, forward as POST }
In src/lib/constants.tsx, name the method and add a test for it:
export const paymentInfoMap = {
// …the existing providers
pp_openreceive_openreceive: { title: "Bitcoin & stablecoins", icon: <CreditCard /> },
}
export const isOpenReceive = (providerId?: string) =>
providerId?.startsWith("pp_openreceive_")
In src/modules/checkout/components/payment-button/index.tsx, show the
checkout for an OpenReceive session and place the order once it settles:
import { isManual, isOpenReceive, isStripeLike } from "@lib/constants"
import { Checkout } from "@openreceive/react"
// in PaymentButton's switch, before the manual-payment case:
case isOpenReceive(paymentSession?.provider_id) && !!paymentSession:
return <OpenReceivePaymentButton notReady={notReady} sessionId={paymentSession!.id} />
const OpenReceivePaymentButton = ({ notReady, sessionId }: { notReady: boolean; sessionId: string }) => {
const [errorMessage, setErrorMessage] = useState<string | null>(null)
if (notReady) return <Button disabled>Place order</Button>
return (
<>
<link rel="stylesheet" href="/api/openreceive/styles.css" precedence="default" />
<Checkout
reference={sessionId}
prefix="/api/openreceive"
resumable
onSettled={() => {
placeOrder().catch((err) => setErrorMessage(err.message))
}}
/>
<ErrorMessage error={errorMessage} data-testid="openreceive-payment-error-message" />
</>
)
}
The payment step needs no change: it already starts a payment session for the method the buyer picks.
Another storefront
Render the checkout where the buyer confirms the order, with the cart’s
OpenReceive payment session id as its reference: React <Checkout>, or the
<openreceive-checkout> element from @openreceive/elements. When it reports
the payment settled, complete the cart (POST /store/carts/{id}/complete).
Its calls go to /store/openreceive on the Medusa server with the
publishable key: forward them from your storefront’s server as above, or call
Medusa from the browser with a fetch that adds the
x-publishable-api-key header. A browser call needs the storefront’s origin
in Medusa’s STORE_CORS, as the storefront’s own browser calls do; from
another site than Medusa’s, the origin must be listed there exactly, not as a
pattern.
How a payment works
- The buyer picks “Bitcoin & stablecoins” at the payment step, and Medusa starts a payment session with OpenReceive for the cart’s total.
- The review step shows the checkout. It charges the session’s amount in the cart’s currency, converted to sats at the current BTC price. Whoever holds the session’s id may pay it and follow it, as whoever holds a cart’s id may complete it in Medusa’s own store API.
- Once the wallet reports the payment, the storefront places the order. Medusa refuses to complete the cart before that.
- The order’s payment is captured at once, and its data holds the Lightning payment hash.
If the buyer closes the page after paying, the plugin’s job finds the payment within a minute or so, and Medusa’s process-payment workflow places the order anyway. If the cart changes before the buyer pays, Medusa starts a new payment session, and the checkout asks for the new total.
Refunds and removal
The receive-only wallet cannot send refunds. A refund made in the Medusa admin is recorded on the order, as for Medusa’s manual payments, and the plugin logs a warning naming the payment: send that refund from your own wallet. A buyer whose swap deposit never converted reclaims it from the same checkout while the cart is still open. If you turn on swaps, keep that path available; see swap refunds.
Removing the plugin keeps its two tables: they are a record of payments received.
Tested, and not tested
Tested on 2026-10-16 with Medusa 2.21.2 (Medusa’s default starter) and
PostgreSQL 17 in Docker, the Next.js starter, a test wallet and a test swap
provider: db:migrate with the plugin’s tables, the enable script, doctor,
and a buyer paying an order by Lightning and by USDT on Solana in Chromium,
after which the order is placed with its payment captured. A payment whose
buyer never placed the order was placed by the plugin’s job. An unknown
session, another provider’s session and a request without the publishable
key were refused, and an admin refund was recorded.
Not tested: Medusa split into a server and a worker, Medusa Cloud, storefronts other than the Next.js starter, and a storefront calling Medusa from the browser on another site. Lightning cannot pull a payment, so OpenReceive saves no payment methods.