Remix
Use the Fetch handler from your resource route and keep the wallet connection server-only.
- Server package
@openreceive/express- API mount
/openreceive/v1- Secret location
- Server only
Common setup
First, get a receive-only NWC code so your server can create invoices and check payment status. You can switch NWC providers later without changing the browser checkout.
OPENRECEIVE_NWC=nostr+walletconnect://...
OPENRECEIVE_STORE=local-sqlite
OPENRECEIVE_NAMESPACE=default
Use postgres://... for production storage, or
sqlite:///abs/path/openreceive.sqlite3 for an explicit single-machine file.
The store initializes itself on boot.
npm install @openreceive/node @openreceive/express @openreceive/browser pg
npx openreceive doctor
Remix server route
Live checkout always needs a server component. Browser code never receives
OPENRECEIVE_NWC.
import {
createOpenReceiveFetchHandler,
createOpenReceiveFetchRuntime,
createOpenReceiveNodeHandler
} from "@openreceive/express";
import {
createAlbyNwcReceiveClient,
formatOpenReceiveMissingNwcMessage,
resolveOpenReceiveStore
} from "@openreceive/node";
let runtime;
const store = await resolveOpenReceiveStore();
function getRuntime() {
if (runtime) return runtime;
const nwc = process.env.OPENRECEIVE_NWC;
if (!nwc) {
const message = formatOpenReceiveMissingNwcMessage();
console.error(message);
throw new Error(message);
}
runtime = createOpenReceiveFetchRuntime({
client: createAlbyNwcReceiveClient({
connectionString: nwc
}),
store,
merchantScope: () => "app:default",
auth: {
create: (req) => isAllowedToCreateInvoice(req),
read: (req, invoice) => ownsInvoice(req, invoice),
lookup: (req, invoice) => ownsInvoice(req, invoice),
refresh: (req, invoice) => ownsInvoice(req, invoice),
poll: (req) => isInternalScheduler(req)
},
csrf: {
verify: (req) => verifyCsrf(req)
},
settlementAction: async ({ invoice, metadata }) => {
await markOrderPaid({
invoiceId: invoice.invoice_id,
orderId: metadata.order_id
});
}
});
return runtime;
}
export const openreceive = createOpenReceiveFetchHandler({
runtime: getRuntime
});
export const openreceiveNode = createOpenReceiveNodeHandler({
runtime: getRuntime
});
import { openreceive } from "~/server/openreceive";
export async function loader({ request }) {
return openreceive(request);
}
export async function action({ request }) {
return openreceive(request);
}
Browser checkout
Your UI creates an invoice by posting to your OpenReceive server route.
const response = await fetch("/openreceive/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": orderId
},
body: JSON.stringify({
fiat: {
currency: "USD",
value: "10.00"
},
metadata: {
order_id: orderId
}
})
});
const invoice = await response.json();
import { OpenReceiveCheckout } from "@openreceive/react";
import "@openreceive/react/styles.css";
export function Checkout({ invoice }) {
return (
<OpenReceiveCheckout
{...invoice}
lookupUrl="/openreceive/v1/invoices/lookup"
/>
);
}
No-framework apps can use @openreceive/elements or lower-level @openreceive/browser helpers.
Recovery
OpenReceive does not need a daemon or wallet notification listener. Browser lookups and bounded route-triggered sweeps use backend invoice lookup, and an optional scheduler can run one extra recovery pass.
web npm start
optional scheduler npx openreceive poll --once
npx openreceive poll --once