OpenReceive for Rails
Accept Bitcoin & Stablecoin Payments With Rails
Add Bitcoin Lightning and stablecoin checkout to your Rails app with one gem and three hooks. Your models keep the orders; the engine only settles the payment.
Tested live · Oct 6, 2026 · 0.4.18
Copy agent directions — Paste into Claude Code, Codex or Cursor to add OpenReceive to your Rails app.
Why OpenReceive
- No OpenReceive account. There is no sign-up, no API key and no dashboard. Install the package, add a receive-only wallet code, and your server issues invoices itself.
- Open source, inside your server. MIT-licensed routes mount in the app you already run. OpenReceive never sees an order, a price or a customer, and never holds funds.
- Receive-only wallet permissions. The one credential is a receive-only NWC code. It can create invoices and read their status. It cannot spend, so a compromised server cannot drain the wallet.
- Stablecoins, optional. Connect a swap provider and customers can pay in USDT, USDC, ETH or SOL. Every payment still settles to your wallet in Bitcoin.
Try the checkout your customers will see
This checkout is the Rails demo shop that runs openreceive.org itself: the same gem, engine and hooks the quickstart below describes. The live demo needs JavaScript: open the OpenReceive home page.
Set up
Requires Ruby ≥ 3.2, Rails ≥ 8.0.
1. Get a receive-only NWC code — get one here. It can issue invoices and watch for payment, and it can never spend from your wallet.
2. Get an LSC code for swaps (optional) — from a swap provider, only if customers should be able to pay in USDT, USDC, ETH or SOL.
3. Set the environment variables — NWC_URI (required) and
LSC_URI_PRIMARY / LSC_URI_BACKUP (optional) in the environment of
your Rails server process; see
Environment variables.
4. Install
bundle add openreceive-rails
5. Wire OpenReceive — quoted from the Rails quickstart.
OpenReceive.configure do |config|
# `Order` throughout is YOUR model — it could be named anything. OpenReceive
# never sees it or touches its table; these hooks are the only bridge
# between the engine and your data.
#
# Your policy, called before every checkout/payment/swap request. `context`
# is a Hash with three symbol keys:
# context[:action] — which route: "checkout.prepare", "checkout.create",
# "payment.check", "swap.quote", "swap.create",
# "swap.read", or "swap.refund"
# context[:request] — the ActionDispatch::Request; read your session,
# cookies, or headers from it, as in a controller
# context[:resource] — { reference:, payment_hash: } copied from the
# payer's JSON body. It names an order; it does not
# prove this caller owns it. reference is always a
# validated non-empty String (≤200 chars); payment_hash
# is nil except on payment.check / swap.read / swap.refund.
# Return true to allow, false for a 403. Here: only the signed-in customer
# who placed the order may act on it.
config.authorize = lambda do |context|
order = Order.find_by(id: context[:resource][:reference])
order && order.user_id == context[:request].session[:user_id]
end
# The price for a reference — here, your order id — from your own data;
# nil when there is nothing to pay for (a 404). `value` is a decimal STRING
# from the order row, never a float and never a request param. `description`
# is what the payer is buying, in your own words.
config.amount_for = lambda do |reference|
order = Order.find_by(id: reference)
order && { currency: "USD", value: order.total.to_s,
description: "#{order.line_items.size} items" }
end
# Runs inside the settlement transaction, only for the order's first settled
# attempt. The WHERE clause is the lock: a second fulfillment path of yours
# (admin action, replayed job) updates zero rows and does nothing. Plain
# ActiveRecord, because the engine WRAPS this block in the transaction.
# (The JS engine instead hands onPaid a `query` handle, since nothing wraps
# it there; that is the one shape difference between the two stacks.)
config.on_paid = lambda do |settlement|
claimed = Order
.where(id: settlement.reference, state: "awaiting_payment")
.update_all(state: "paid", paid_at: Time.at(settlement.paid_at).utc)
next if claimed.zero?
end
end
6. Render the checkout
<%# app/views/orders/pay.html.erb %>
<openreceive-checkout reference="<%= @order.id %>"></openreceive-checkout>
Full Rails quickstart · Example app
Practical questions
- Which currencies can customers pay with?
- Bitcoin over Lightning, always. With a swap provider configured, also USDT on Tron, Solana or Ethereum, USDC on Solana or Ethereum, ETH and SOL, each converted into the same Lightning payment. Automated swaps guide
- Where does the Bitcoin go?
- Into the wallet behind your NWC code. OpenReceive never holds funds: the invoice is minted in your wallet and the sats land there. Your server confirms settlement and runs your onPaid hook once per order. Payment storage guide
- What wallet do I need?
- Any wallet that issues receive-only NWC (Nostr Wallet Connect) codes, hosted or self-run. The code must be able to create invoices and read their status, and must not be able to spend. Get a receive-only NWC code
- What does it cost?
- OpenReceive is open source and free. Fees charged by your wallet provider, by a swap provider or by Lightning routing belong to those services and are outside OpenReceive's scope.
- What do I need to deploy?
- Ruby 3.2 or later, Rails 8.0 or later, NWC_URI in the server environment and the engine's migration run against your database. Settlement runs on the mounted engine routes; a notification worker is optional. Deploying guide
Add OpenReceive to your Rails app
Copy agent directions · Full quickstart · Example repository · API reference · Guides