Skip to content

Repository files navigation

OpenReceive

Freedom technology for inbound payments.

Accept Bitcoin payments on your website, app, or point of sale, straight into a wallet you control.

Bitcoin

Bitcoin by default. Use the internet's neutral settlement currency. Your server issues a QR code. The payer pays the QR code, and your server approves delivery of the purchase.

Deposit-only by design. OpenReceive exposes no payment-sending API and never holds a key: it connects with only a spec-compliant receive-only NWC code. Choose an existing NWC service to receive payments, or build your own NWC Service.

To run the wallet on your own hardware, use an NWC service you host yourself, like Alby Hub.

Optionally swap in other currencies. Not every customer holds Bitcoin. Configure any swap provider to receive altcoins. Use any swap provider that implements the FixedFloat / Lightning-Swap API, or build your own.

Optional Inbound Currencies:

Pay with On network Settles in
USDT  USDT (Tether) Tron Tron    Solana Solana    Ethereum Ethereum Bitcoin Bitcoin
USDC  USDC (USD Coin) Solana Solana    Ethereum Ethereum Bitcoin Bitcoin
SOL  SOL (Solana) Solana Solana Bitcoin Bitcoin
ETH  ETH (Ether) Ethereum Ethereum Bitcoin Bitcoin

Install

# Node: an HTTP adapter for your framework, plus the checkout UI for yours
npm install @openreceive/express @openreceive/react

Swap @openreceive/express for @openreceive/fastify or @openreceive/next, and @openreceive/react for @openreceive/vue, @openreceive/svelte, @openreceive/angular, or @openreceive/elements (framework-free custom element). On Rails:

# Gemfile
gem "openreceive-rails"

Quickstart

Pick your stack:

Stack Quickstart
Node.js Node quickstart
Ruby on Rails Rails quickstart
BTCPay Server Coming soon

On all platforms: Your server owns the price and the order, the payer gets a QR code to pay, and your onPaid hook runs once inside the settlement transaction.

Security defaults

  • OpenReceive does not transmit money or hold customer funds. OpenReceive only helps your backend create payment QR codes and safely verify settlement.
  • OpenReceive cannot spend your funds.
    1. An attacker who gains control of your server gets no reward: A receive-only NWC code cannot spend.
    2. Every payment settles as a private, immutable Bitcoin Lightning payment, swapped from other currencies as necessary. Accept ETH, SOL, USDT, and USDC without censorship risks.
    3. See Security guide.
  • Your app owns business state. Your application owns orders; the library owns the openreceive_payments rows (they live in your database) — see Payment storage. OpenReceive never owns orders, users, prices, or fulfillment, and never requires a separate database, Redis, or migration runner: you pass a database handle, and the library owns the schema, locking, settlement write-once, and reconciliation.

How it fits into your app

OpenReceive is three server objects plus an optional browser package. Each one talks to a different side of your app, and each has an obvious home:

Piece You build it with It talks to It lives
Wallet client createOpenReceive() your wallet — mints invoices, reads settlement, holds the NWC code server-only, one per process
Host createHost() your database — your hooks, plus the openreceive_payments table server-only, next to your models
HTTP routes openReceiveExpress() (or Fastify / Next / Rails) the browser — the endpoints the checkout UI calls mounted on your app by default at /openreceive
Checkout UI (optional) @openreceive/react (or vue/svelte/angular/elements) the HTTP routes above — creates the checkout, polls until paid your browser bundle

Only that last row is genuinely optional. Take the drop-in components, build your own on the semver-stable @openreceive/browser/headless engine, or skip our browser packages altogether and call the routes yourself — the server side is identical either way. See Headless checkout.

The three server pieces:

import { openReceiveExpress } from "@openreceive/express";
import { createHost } from "@openreceive/http";
import { createOpenReceive } from "@openreceive/node";

// 1. The wallet client. Reads NWC_URI; never let this reach client code.
const service = await createOpenReceive();

// 2. The host: your database and your price.
const host = createHost({
  db, // pg Pool/Client, node:sqlite, better-sqlite3, or a custom adapter
  // The authoritative price for a reference (here, your order id) — never
  // taken from payer input.
  // OpenReceive converts this exact decimal into the invoice amount; null
  // means there is nothing to pay for (404).
  amountFor: async (reference) => {
    const order = await orders.find(reference);
    return order
      ? { currency: order.currency, value: order.total.toString() }
      : null;
  },
  onPaid: async ({ reference, query }) => {
    // Runs inside the settlement transaction, only for the order's first
    // settled attempt. Update the order or insert an outbox row here.
    await query("UPDATE orders SET state = 'paid' WHERE id = ?", [reference]);
  },
});

// 3. The HTTP routes. `authorize` is your own access check: it runs on every
//    reference-scoped request, because a reference alone does not prove ownership.
app.use(
  openReceiveExpress({
    service,
    host,
    authorize: async ({ action, request, resource }) =>
      orders.authorize({ request, reference: resource.reference, action }),
  }),
);

The OpenReceive host

The host is the server object between your app and OpenReceive's payment attempts: it calls the two hooks you hand it — amountFor, onPaid — and owns one table, openreceive_payments, inside your database. You run that table's migration (npx openreceive scaffold payments emits it for your ORM); the library owns everything else: schema, per-reference locking, write-once settlement, reconciliation.

The reference is a string you choose, and it is the fulfillment identity: your order id — one per thing you fulfill, created before checkout, kept across retries, never reused. OpenReceive never looks inside it, but onPaid runs once per reference, a new checkout under a reference that already settled is refused with 409, and a fresh id per page load lets one order be paid twice. Each row is one invoice or swap attempt under a reference. A row commits before the payer sees an invoice, settles once, and fulfills at most once per reference; to your app an order is simply unpaid or paid.

Schema, the attempt state machine, live-attempt rules, and the custom-repository escape hatch: Payment storage.

Only one secret required to get started

createOpenReceive() reads the receive-only wallet code from NWC_URI; optional swap providers come from LSC_URI_PRIMARY and LSC_URI_BACKUP. Those are OpenReceive's only secret environment variables. See Environment variables.

The routes run your authorize on every request

The routes never inspect your session. You write one callback, authorize (step 3 above), and it runs on every reference-scoped request — a reference identifies a row but does not prove the caller owns it. The context carries the action (checkout.create, payment.check, …), the Web-standard request, and the untrusted resource selectors the payer sent; return false for 403.

A create request carries a reference, never a price: your amountFor hook resolves the amount. A refused attempt (order already paid, competing live attempt) is a 409 with no invoice attached. Authorization covers the context object, framework sessions, and guest orders.

Settlement is decided by the wallet

OpenReceive checks your wallet for payment while serving the checkout routes, so no background process is required. An order is marked paid only when the wallet itself reports the payment final — never from a preimage the payer presents, never from a swap provider reporting "complete" — and an unpaid invoice is closed only once the wallet confirms it went unpaid, not on your server's clock. Optional notification workers (Rails: rake openreceive:notifications) settle faster under the same rule.

How settlement is driven, multi-instance behavior, and workers: Deploying. Swap recovery, swap_data, and refunds: Automated swaps.

Writing your own checkout route

Most applications should not — mounting the adapter gives you the routes, and the shipped checkout components work against them with no glue. If you need a flow the routes do not offer: Writing your own checkout route.

Run a demo

One shop, four stacks. You add buttons to a cart, check out to create an order, and pay that order with a real Lightning invoice from your own wallet or a stablecoin swap; the download unlocks only after onPaid marks the order paid.

npm run demo node      # Buy a Button — Express + React/Vue/Svelte/Angular  http://localhost:3000
npm run demo static    # Buy a Button — static HTML, no framework           http://localhost:3001
npm run demo nextjs    # Buy a Button — Next.js app router                  http://localhost:3002
npm run demo buttons   # Buy a Button — Rails + host Postgres               http://localhost:3003

Buy a Button is the persistence story: a products table, a visitor remembered by a signed cookie, an orders table, and a public feed of every paid order on the site, with three lambdas as the entire bridge to OpenReceive. The four stacks share one shop — the UI, the wire types and the Node server live once in examples/buttons/shared/ and each stack under server/ is a thin host with its own routing, database idiom and build.

They differ in exactly two interesting ways, and both are on purpose: Rails pushes settlement over ActionCable while the Node stacks poll, and node-express plugs the packaged <Checkout> into the shared shop behind React / Vue / Svelte / Angular tabs while the others render the keystone-driven checkout.

Every demo needs a receive-only NWC_URI in the root .env. The Buy a Button README explains what each command starts and which parts of the code belong to the shop and which to the library.

Development

npm test               # the JS suite
npm run check          # contracts and secret-safety checks
npm run test:ci        # the full deterministic gate, including Ruby and demos

CONTRIBUTING has setup, ground rules, and the repository layout; the test command map lists every command.

Documentation

Start with the developer guides:

About

Freedom tech for inbound payments.

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages