# OpenReceive + Stripe

Keep Stripe for cards and add BTC, USDT & USDC, paid straight into a wallet you
control. BTC arrives over Lightning, and a swap provider converts USDT and USDC
to BTC on the way in. This page is the design we run in production next to
Stripe, and agent directions that build it into your app.

## Tested in production

This is not a sketch. Since June 2026 this design has run a subscription
software business, with Stripe and OpenReceive side by side on one pricing page:

- Monthly plans and credit packs by card, through Stripe.
- Prepaid passes of 60, 180 and 365 days, and the same credit packs, in BTC,
  USDT & USDC through OpenReceive.
- Customers who hold a card plan and a prepaid pass at the same time.

Everything on this page comes from running it, including the mistakes we fixed.
The agent directions below build the same design into your app.

## What changes, and what doesn't

**Stripe stays as it is.** Checkout, subscriptions, the customer portal,
webhooks, receipts and tax settings keep working exactly as they do today. Your
customers who pay by card see no difference.

**OpenReceive adds a second way to pay.** Next to "Pay with card" your pricing
page gets "Pay with BTC, USDT & USDC". The invoice comes from your own Lightning
wallet through a receive-only connection code, so there is no account to open,
no payout to wait for and no one holding your money. A configured swap provider
turns USDT and USDC payments into BTC on the way in.

**Both end in the same place.** A card payment and an OpenReceive payment run
the same piece of your code: the one that grants what was bought. Everything
after that (access, credits, features) doesn't care how the customer paid.

## Why BTC, USDT & USDC plans are prepaid passes

A card subscription works because Stripe keeps the card on file and charges it
every month. Nobody can pull BTC, USDT or USDC out of a customer's wallet. Each
payment is one the customer sends, so there is nothing to bill next month.

So the two payment methods sell time differently:

| | Card (Stripe) | BTC, USDT & USDC (OpenReceive) |
| --- | --- | --- |
| Plans | Monthly or yearly, renews automatically | A prepaid pass of 60, 180 or 365 days, never renews |
| Paid | Every period | Once, up front |
| Credits | Each month | The whole pass's worth, at purchase |
| Credit packs | Same price | Same price |
| Chargebacks | Possible | None |

## Pricing a prepaid pass

Start from the card price. A pass costs the monthly price times the months it
covers, less a discount for paying up front: about 10% for 60 days, 20% for 180
days and 26% for 365 days. Count 365 days as 365/30 months, and round to whole
dollars.

| Plan | Card, per month | 60-day pass | 180-day pass | 365-day pass |
| --- | --- | --- | --- | --- |
| Basic | $19 | $34 | $91 | $171 |
| Plus | $39 | $70 | $187 | $351 |
| Business | $99 | $178 | $475 | $891 |

The discount is fair, not a giveaway. You get the money up front, with no card
fees, no failed renewals and no chargebacks.

- **Show 60 days first.** It is the easiest yes.
- **Credits arrive all at once.** A pass includes the plan's monthly credits
  times days ÷ 30, granted when it is paid, and they expire when the pass ends.
- **Passes stack.** Buying a pass while one is running adds its days to the end
  of the current one. Let customers renew early. Stacking only works if the buy
  button stays enabled.
- **Say it clearly.** Put "One-time pass, no auto-renew" next to every pass
  price.
- **Remind people.** Email 7 days before a pass ends and on the day it ends. A
  pass that lapses quietly is a customer you lose without them deciding to
  leave.

## Credit packs

If you sell usage, credit packs cost the same in dollars by card or in BTC, USDT
& USDC, and they never expire. When a customer has both, spend the plan's or
pass's credits first, since those expire, and pack credits last.

## When a customer has both

These rules come straight from production, where getting them wrong cost us a
customer's paid time.

- **A live card plan blocks prepaid passes.** While a card subscription is live,
  even one set to cancel at the period end, the pass buttons are disabled and
  the server refuses them: "You have a card plan. Prepaid passes become
  available once it ends. Credit packs are available any time."
- **A pass first, then a card plan: both run.** The pass keeps its days. The
  customer gets the features of the better plan, and the credits add up.
- **A pass paid late still counts.** A customer can prepare an OpenReceive
  checkout, start a card plan, and pay that invoice afterwards. That pass runs
  as a normal pass on its own record.
- **Never write a pass onto a card subscription's record.** If a prepaid pass
  updates the same row that Stripe keeps alive, Stripe keeps charging while the
  app thinks the customer is on a pass. Keep card and pass access separate, and
  let only the Stripe sync touch a row that holds a Stripe subscription.

## The design

This is what the agent directions build. It fits any framework OpenReceive
supports.

```text
Stripe webhook ─────────┐
                        ├──▶ settle(order) ──▶ grant plan time, credits or the product
OpenReceive on_paid ────┘
```

1. **One order table for both.** Every purchase is an order row. It records
   which payment method it used, what kind of purchase it is (a card plan
   period, a prepaid pass, a credit pack or a product), and the price in
   dollars. Revenue reports read dollars for both, never satoshis.
2. **One settle function.** The Stripe webhook and OpenReceive's `on_paid` hook
   both call it. It locks the order, does nothing if the order is already paid,
   and grants what the order bought. Every grant has a unique key, so a replayed
   event can never grant twice.
3. **Separate access records.** A card plan's access lives on its Stripe
   subscription record, which only the Stripe sync updates. A prepaid pass lives
   on its own record, with a paid-until date. One helper reads both and decides
   what the customer can do.
4. **The OpenReceive order comes first.** When the customer clicks "Pay with
   BTC, USDT & USDC", the app creates the order with its dollar price and an
   opaque, never-reused reference. OpenReceive's `amount_for` returns that saved
   price, and `authorize` checks that the order belongs to the signed-in
   customer. OpenReceive converts dollars to BTC at checkout and refuses when
   the exchange rate is stale.
5. **Stripe is store-then-process.** Verify the signature, save the event, and
   answer 200. Then process it, re-reading the subscription from Stripe, because
   events arrive out of order. Nothing is granted on the success redirect.
6. **OpenReceive needs no webhook.** The checkout polls, every OpenReceive
   request checks for payments first, and an optional worker makes it instant
   when no browser is open. `on_paid` runs inside OpenReceive's database
   transaction, so emails and live updates go out after it commits.
7. **The checkout link is the refund link.** If a USDT or USDC deposit arrives
   short or late, the swap provider refunds it, and the customer claims it on
   the same checkout page. Keep that page reachable from the order.

## Get the agent directions

Pick your situation and your framework at the top of this page, and copy the
directions into Claude Code, Codex, Cursor or another coding agent. There are
two sets:

- **Add to my Stripe app.** The agent finds the code your Stripe webhook runs
  when a payment succeeds and reuses it for BTC, USDT & USDC. It adds the
  OpenReceive order, passes and buttons, and leaves Stripe alone. Your existing
  tests have to keep passing.
- **Start a new app with both.** The agent builds the whole design: card plans
  and credit packs through Stripe Checkout, prepaid passes and packs through
  OpenReceive, one settle function and one access helper.

Either way, the agent first asks for two codes from your wallet and swap
provider, one at a time. Then it asks one question about your pass prices, where
the defaults above are a fine answer.

A coding agent reading this page can fetch the directions itself:
`https://openreceive.org/agent-directions/stripe/add/<stack>/full.md` to add to
an app that already uses Stripe, or `…/stripe/new/<stack>/full.md` for a new
app. `<stack>` is `node` (Express), `fastify`, `next`, `rails`, `django`,
`fastapi`, `laravel` or `php`.

## What the agent won't touch

- Your Stripe prices, products, webhook endpoint or API version
- How existing card customers are billed
- Your Stripe secret key, except in a new app where it sets one up
- Your existing tests, which must still pass

## Questions

**Can I refund a BTC, USDT or USDC payment?** Not automatically. The connection
code can receive but never send, which is what keeps your wallet safe. Refund
from your wallet by hand if you need to. USDT and USDC deposits that arrive
short or late are refunded to the payer by the swap provider. The customer
claims the refund on the checkout page.

**Receipts?** Stripe emails card receipts. For OpenReceive orders, show the
order on the customer's account page or send your own email after `on_paid`
commits.

**Where does the money go?** To the Lightning wallet you connected. USDT and
USDC payments arrive there as BTC, converted by the swap provider. Which
networks are available depends on the provider.

**Which frameworks?** Express, Fastify, Next.js, Rails, Django, FastAPI, Laravel
and plain PHP. On WordPress with WooCommerce, install the OpenReceive plugin,
and it appears next to the Stripe gateway at checkout. Java, Go and .NET servers
aren't supported yet. If you sell through Stripe Payment Links or a site builder
with no server of your own, start from [a
platform](https://openreceive.org/platforms) instead.
