openreceive 0.4.21 → 0.4.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,531 @@
1
+ This is the full file; follow it from Step 0.
2
+
3
+ # OpenReceive agent directions (Lovable)
4
+
5
+ ```text
6
+ READ THIS FIRST: this file is 25 KB and a summary drops required steps. Read all of it:
7
+ https://openreceive.org/agent-directions/lovable/full.md
8
+ If your fetch tool gave you a summary, fetch that URL again for the full text.
9
+ ```
10
+
11
+ **Step 0 in brief** (Step 0 below has the details):
12
+
13
+ - Before adding packages or editing anything, ask the user for a receive-only NWC code. One question per message.
14
+ - Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
15
+ - Ask for NWC_URI and LSC_URI_PRIMARY with Lovable's secret input. Never put a code in .env, code or a VITE_ name.
16
+ - You have no terminal: never ask the user to run a command. Every step is a file, a package, a migration or a secret.
17
+ - Do not suggest rotating or revoking a code because it was pasted here.
18
+ - Start Step 1 only once NWC_URI is saved, and LSC_URI_PRIMARY is saved or the user said "Bitcoin only".
19
+
20
+ These directions describe OpenReceive 0.4.23.
21
+
22
+ Add OpenReceive to this Lovable app: a TanStack Start app on Cloudflare
23
+ Workers, with Lovable Cloud or a connected Supabase project as its database.
24
+ These directions are written for Lovable's agent. You have no terminal, so
25
+ every step is a file edit, a package, a Supabase migration or a secret. Never
26
+ ask the user to run a command. The TanStack Start recipe is appended to this
27
+ file in full; its "On Supabase (Lovable)" section is the code to follow.
28
+
29
+ Do not clone the OpenReceive repository, and do not copy a demo's models over
30
+ tables that already exist. Find this app's orders and products, whatever they
31
+ are named, and map the hooks onto those. Keep this app's UI, sign-in and
32
+ database.
33
+
34
+ ## What OpenReceive is
35
+
36
+ A payment library that runs inside THIS app's server code. It mounts as one
37
+ TanStack Start server route (`src/routes/openreceive.$.ts`), issues Lightning
38
+ invoices against a wallet the merchant already controls, and settles each
39
+ payment into this app's Supabase database. There is no OpenReceive account
40
+ and no API key, and OpenReceive never holds the funds: the sats land in the
41
+ merchant's wallet.
42
+
43
+ The one required credential is a receive-only NWC code (Nostr Wallet
44
+ Connect): a string from the merchant's wallet that can create invoices and
45
+ read their status, and cannot spend. A swap provider (an "LSC" code)
46
+ optionally lets the payer send USDT, USDC, ETH or SOL instead, converted into
47
+ that same Lightning payment. Step 0 collects those credentials. You write two
48
+ hooks in TypeScript, `amountFor` and `authorize`, and one SQL function,
49
+ `openreceive_on_paid`, that marks the order paid. OpenReceive supplies
50
+ invoices, polling, settlement and the checkout UI. It never owns orders,
51
+ users, prices, or fulfillment.
52
+
53
+ On Lovable, OpenReceive keeps its payment rows in this app's Supabase
54
+ database and reaches it over Supabase's HTTPS API with the project's secret
55
+ key, because a Worker cannot open a Postgres connection to Supabase.
56
+
57
+ ## Step 0 — ask for the two codes, one secret at a time
58
+
59
+ If the user says the codes are already saved as this project's secrets
60
+ (`NWC_URI`, and `LSC_URI_PRIMARY` unless they want Bitcoin only), believe
61
+ them: do not ask for them, and go to Step 1. You cannot read a secret's value,
62
+ and you do not need to.
63
+
64
+ Otherwise, before anything else, your next action is a question to the user.
65
+ Do not add packages, write migrations or edit files before asking it. A new
66
+ shop has neither code yet.
67
+
68
+ Two server-only secrets are needed before the integration:
69
+
70
+ - `NWC_URI` — a receive-only Nostr Wallet Connect code,
71
+ `nostr+walletconnect://…`. Required for Bitcoin.
72
+ - `LSC_URI_PRIMARY` — a Lightning Swap Connect URI,
73
+ `lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
74
+ only when the user says they want Bitcoin alone.
75
+
76
+ Ask for each one with Lovable's secure secret input, named exactly as above.
77
+ The user never edits a file: they paste each code into this chat or into
78
+ that secret input, and Lovable stores it as a project secret. A code pasted
79
+ into the chat is saved as a secret too. Never repeat a code back. Ask one
80
+ question per message.
81
+
82
+ 1. **First message — the NWC code, and nothing else.** Ask for the secret
83
+ `NWC_URI` and walk them through getting it:
84
+
85
+ > To receive payments I need a receive-only wallet code. In Rizful: open
86
+ > the menu, tap NWC, choose Receive-only NWC code, and tap Copy
87
+ > (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
88
+ > rather run your own wallet, Alby Hub works too: Connections → Add
89
+ > Connection → Read Only. Paste it into the secret input for NWC_URI.
90
+
91
+ 2. **When it is saved.** If the user pasted it into the chat and it does not
92
+ start with `nostr+walletconnect://`, ask them to copy the receive-only code
93
+ again. Reply only that it is saved, then ask the next question.
94
+ 3. **Second message — swaps.** If the user asked for stablecoins, USDT, USDC,
95
+ ETH, SOL, altcoins or "crypto" (as in "Bitcoin and stablecoin payments"),
96
+ this message IS the walkthrough below: send it as it is, and do not ask yes
97
+ or no first. Otherwise ask whether payers should also be able to pay with
98
+ USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:
99
+
100
+ > Go to https://lightning-swap.com, sign in for API keys, create a key, and
101
+ > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
102
+ > it into the secret input for LSC_URI_PRIMARY — or say "Bitcoin only" and
103
+ > I will continue without it.
104
+
105
+ Mention FixedFloat only if they already use it. Swaps on means the refund
106
+ route back is part of this integration (the swap non-negotiable below).
107
+ 4. **Supabase.** Lovable sets `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`
108
+ for server code when Lovable Cloud, or a connected Supabase project, is on.
109
+ Do not ask for them or try to create them: Lovable reserves the
110
+ `SUPABASE_` prefix. If this project has no backend yet, ask the user to
111
+ turn on Lovable Cloud, and wait until they have.
112
+
113
+ Never give a code a `VITE_` prefix, never put one in `.env` (Lovable commits
114
+ that file), and never write one into code or a log. Do not invent placeholder
115
+ codes. Start Step 1 only once `NWC_URI` is saved and `LSC_URI_PRIMARY` is
116
+ saved or explicitly declined.
117
+
118
+ ## Step 1 — the packages
119
+
120
+ Add `@openreceive/http` and `@openreceive/react` at 0.4.23 or newer.
121
+ Nothing else: no `pg`, no Postgres driver, no OpenReceive scaffold. Do not
122
+ change the Vite or Wrangler configuration.
123
+
124
+ ## Step 2 — the migration
125
+
126
+ Fetch https://openreceive.org/guides/supabase-migration.md and copy its SQL
127
+ block, unchanged, into one new Supabase migration, then apply it. Do not edit
128
+ it, split it, rename anything in it, or drop its `revoke` lines. It creates
129
+ `openreceive_payments` and `openreceive_meta` with row level security on and
130
+ every grant revoked from `anon` and `authenticated`, the functions the server
131
+ calls to write them, and `openreceive_on_paid` as a placeholder that refuses
132
+ every settlement.
133
+
134
+ ## Step 3 — the order and its buyer
135
+
136
+ - **The order's id is the `reference`.** Create the order before checkout,
137
+ keep it across retries, never reuse it. A fresh id per page load lets one
138
+ order be paid twice.
139
+ - **The order row is the price.** Order creation copies live prices into the
140
+ order. `amountFor` reads only that order and returns
141
+ `{ currency, value, description }`, the value a decimal string such as
142
+ `"12.50"`, and `null` for an order that is not payable.
143
+ - **The buyer is a cookie.** The checkout calls the payment routes from the
144
+ browser with the page's cookies and nothing else, and a Lovable app keeps
145
+ its Supabase sign-in in the browser, not in a cookie. So in a migration add
146
+ `buyer_token text` to the orders table, and create orders in a server
147
+ function (`createServerFn`) that reads the `buyer` cookie, or sets one to
148
+ `crypto.randomUUID()` (HttpOnly, Secure, SameSite=Lax, path `/`, one
149
+ year) with `getCookie` and `setCookie` from `@tanstack/react-start/server`,
150
+ then inserts the order with `buyer_token` set to it through `supabaseAdmin`
151
+ from `@/integrations/supabase/client.server`, and returns the id. If the app
152
+ creates orders in the browser today, move that insert into this function.
153
+ Keep the order's user column too, if the app has sign-in.
154
+ - **The order is unpaid or paid.** Do not copy attempt statuses (`pending`,
155
+ `expired`, `failed`, `attention`) onto it, and do not add a relation to
156
+ `openreceive_payments`.
157
+
158
+ ## Step 4 — `openreceive_on_paid`
159
+
160
+ In a second new migration, replace the placeholder with the SQL that marks
161
+ this app's order paid. Use `create or replace`, and this app's own table,
162
+ column and status names:
163
+
164
+ ```sql
165
+ create or replace function public.openreceive_on_paid(
166
+ p_reference text, p_payment_hash text, p_paid_at bigint
167
+ ) returns void language plpgsql as $$
168
+ begin
169
+ update public.orders
170
+ set status = 'paid'
171
+ where id = p_reference::uuid -- or just p_reference, if the ids are text
172
+ and status = 'pending';
173
+ end
174
+ $$;
175
+ ```
176
+
177
+ It runs inside the transaction that records the payment, for the order's
178
+ first settled payment only. If it raises, nothing is recorded, and the next
179
+ request retries it. Its `WHERE` on the unpaid status is the guard against a
180
+ second fulfillment. Keep it to database writes.
181
+
182
+ ## Step 5 — the payment route
183
+
184
+ Write `src/routes/openreceive.$.ts` and `src/lib/openreceive.server.ts`
185
+ exactly as the recipe's "On Supabase (Lovable)" section shows:
186
+
187
+ - `storage: { supabase: { url: process.env.SUPABASE_URL, key: process.env.SUPABASE_SERVICE_ROLE_KEY } }`,
188
+ read inside the handler, never at module scope.
189
+ - No `onPaid` and no `db`: `openreceive_on_paid` is the fulfillment.
190
+ - `findOrder` reads the order with `supabaseAdmin`; `authorize` compares the
191
+ request's `buyer` cookie with the order's `buyer_token`.
192
+ - Build the stack inside each request and close it in `finally`.
193
+ - Import the server module only inside the route's handlers, so it never
194
+ reaches the browser bundle.
195
+
196
+ ## Step 6 — the checkout page
197
+
198
+ Add `src/routes/checkout.$reference.tsx` with `<Checkout reference={reference}
199
+ prefix="/openreceive" />` from `@openreceive/react`, and import
200
+ `@openreceive/react/styles.css` there. After the order is created, navigate
201
+ to `/checkout/<id>`. Use the drop-in; do not build your own checkout UI.
202
+
203
+ ## After setup — hand over, then stop
204
+
205
+ You cannot pay the invoice: the code is receive-only. Do not pay, settle or
206
+ mark an order paid, and do not look for a way to.
207
+
208
+ If the checkout answers 503, the server log names the fix: usually the
209
+ migration is not applied, or `openreceive_on_paid` is still the placeholder.
210
+
211
+ Setup ends here. Your last message starts "Setup is finished" and has at most
212
+ five short lines: where to place an order in the preview, the methods the
213
+ checkout offers, and that a small real payment from their own wallet marks
214
+ the order paid. Send nothing after it. Do not list what changed, offer more
215
+ work, or end the message on a question. Name the methods: Bitcoin, plus
216
+ USDT, USDC, ETH and SOL when `LSC_URI_PRIMARY` is saved. Do not mention
217
+ minimums, and never say a coin will not work or will not be offered.
218
+
219
+ ## Non-negotiables
220
+
221
+ The recipe below has the code. These are the rules it cannot state for
222
+ itself.
223
+
224
+ - OpenReceive never owns orders, users, prices, or fulfillment.
225
+ - Keep `NWC_URI` / `LSC_URI_*` server-only: secrets read with `process.env`
226
+ in `.server.ts` modules. Never a `VITE_` variable, `.env`, browser code or
227
+ a log.
228
+ - Do not suggest rotating, revoking or replacing a code because it was pasted
229
+ into this chat; that is the supported path.
230
+ - `SUPABASE_SERVICE_ROLE_KEY` reads and writes every table. Read it only in
231
+ server modules.
232
+ - Never grant `anon` or `authenticated` anything on an `openreceive_*` table
233
+ or function, and never add a row level security policy to those tables.
234
+ The server refuses to serve while they can reach them.
235
+ - The host owns the price. `amountFor` reads it from the order; reject
236
+ payer-supplied amounts.
237
+ - `authorize` runs on every request, and the `resource` it receives is a CLAIM
238
+ the payer made, not proof. Check the `buyer` cookie against the order.
239
+ - Receive-only NWC is required; a spend-capable code fails closed unless
240
+ explicitly overridden. Never set that override.
241
+ - There is NO merchant-initiated refund of a settled Lightning payment, because
242
+ the wallet cannot spend. Swap refunds — a payer reclaiming a deposit that
243
+ never converted — are the only refund OpenReceive performs. Do not build,
244
+ promise, or imply a Lightning refund path.
245
+ - IF YOU TURN SWAPS ON, BUILD THE ROUTE BACK. A deposit that arrives short or
246
+ late is refunded on a SECOND visit, so the order needs its own URL
247
+ (`/checkout/$reference`, with `syncUrl` on `<Checkout>`), and the attempt
248
+ must come back with it: keep the `payment_hash` from `onState` and pass it
249
+ as `resumePaymentHash`. https://openreceive.org/guides/swap-refunds.md
250
+ - Show the payer what they are buying: return a `description` beside the
251
+ price from `amountFor`.
252
+ - HTTP JSON is snake_case; TypeScript APIs are camelCase.
253
+ - Money is integers or decimal strings — never binary floats.
254
+
255
+ ## More documentation
256
+
257
+ Fetch one when the moment comes. Each is raw markdown, so a plain GET is
258
+ enough.
259
+
260
+ - https://openreceive.org/guides/lovable.md — what the user does on Lovable around these steps
261
+ - https://openreceive.org/guides/supabase.md — Supabase over HTTPS: how the storage works, and what the server checks before it serves
262
+ - https://openreceive.org/guides/supabase-migration.md — the migration for Step 2
263
+ - https://openreceive.org/guides/authorization.md — before you write `authorize`
264
+ - https://openreceive.org/guides/storage.md — the payment tables and the attempt state machine
265
+ - https://openreceive.org/guides/frontend-checkout.md — the drop-in's props, including `syncUrl` and `resumePaymentHash`
266
+ - https://openreceive.org/guides/checkout-ux.md — the rules the drop-in already follows
267
+ - https://openreceive.org/guides/provider-registry.md — where the wallet logos and pay tutorials come from: inside the JavaScript, nothing to serve
268
+ - https://openreceive.org/guides/automated-swaps.md — only if `LSC_URI_PRIMARY` is set
269
+ - https://openreceive.org/guides/swap-refunds.md — the refund flow, and the route back to it
270
+ - https://openreceive.org/guides/lightning-swap-connect.md — what an `LSC_URI_*` code actually is
271
+ - https://openreceive.org/guides/environment-variables.md — every variable, and what is deliberately not one
272
+ - https://openreceive.org/guides/price-feeds.md — where the fiat→sats rate comes from
273
+ - https://openreceive.org/guides/rate-limiting.md — before a public shop goes live
274
+ - https://openreceive.org/guides/security.md and https://openreceive.org/guides/deploying.md — before this goes anywhere real
275
+ - https://openreceive.org/guides/api-reference.md — every route, option and error code
276
+ - https://openreceive.org/guides.md — the index, if what you need is not above
277
+
278
+ Questions, or a problem with the library itself:
279
+ https://openreceive.org/contact
280
+
281
+ ---
282
+
283
+ ## The quickstart, in full
284
+
285
+ Inlined verbatim so this file needs no network access. Steps 0–6 above are the setup on Lovable and this recipe is their reference: follow its On Supabase (Lovable) section, not its pg pool, and skip its npm commands. Where the two differ, the steps win.
286
+ The page it comes from is https://openreceive.org/guides/tanstack-start-recipe.
287
+
288
+ ## TanStack Start recipe
289
+
290
+ OpenReceive ships no TanStack Start package. A TanStack Start server route
291
+ takes a Web-standard `Request` and returns a `Response`, and so does
292
+ `@openreceive/http`, so the integration is the route below. Your server code
293
+ creates each Lightning invoice, the payment goes straight to your wallet, and
294
+ payment attempts are stored in your app's Postgres database. There is no
295
+ OpenReceive account and no background worker.
296
+
297
+ Optional swaps let customers pay with **USDT, USDC, SOL, and ETH**. A swap
298
+ provider you configure converts the payment to **BTC over Lightning**, and it
299
+ settles into the same wallet. Available assets and networks depend on the
300
+ provider.
301
+
302
+ This recipe was checked with `@tanstack/react-start` 1.168 and
303
+ `@openreceive/*` 0.4.19. The app was built with Lovable's Vite config for
304
+ Cloudflare Workers and run under workerd: a buyer's checkout showed a real
305
+ Lightning invoice, and a stranger was refused. The Supabase route below runs
306
+ in OpenReceive's CI as a Worker, against Supabase's own Postgres image and
307
+ API server: a buyer's invoice, a stranger refused, one live attempt under
308
+ concurrent requests, and a payment that `openreceive_on_paid` fulfills once.
309
+
310
+ ```sh
311
+ npm install @openreceive/http @openreceive/react pg
312
+ ```
313
+
314
+ ### Where it runs
315
+
316
+ - **Node** (the `node-server` preset, or any Node host): any Postgres.
317
+ - **Cloudflare Workers** (the `cloudflare-module` preset, Lovable's default):
318
+ with Node compatibility, and a Postgres whose certificate a public
319
+ authority signed, such as Neon.
320
+ - **Cloudflare Workers with Supabase**, which is every Lovable app: a Worker
321
+ cannot open a Postgres connection to Supabase, whose certificate comes from
322
+ a private authority. Use [On Supabase](#on-supabase-lovable) below, which
323
+ stores payments through Supabase's HTTPS API instead.
324
+
325
+ Workers ties every socket to the request that opened it, and forbids network
326
+ calls at import. So the route builds its database pool and its OpenReceive
327
+ stack inside each request, and closes both before it responds. Each request
328
+ then opens its own wallet connection; in our test a checkout request took one
329
+ to four seconds. The same code works on Node.
330
+
331
+ ### The payment route
332
+
333
+ Keep the handler in a `.server.ts` module. Route files are also bundled for
334
+ the browser, so the route loads the module only when a request arrives:
335
+
336
+ ```ts
337
+ // src/routes/openreceive.$.ts — every route under /openreceive/
338
+ import { createFileRoute } from "@tanstack/react-router";
339
+
340
+ async function openReceive({ request }: { request: Request }): Promise<Response> {
341
+ const { handleOpenReceive } = await import("@/lib/openreceive.server");
342
+ return handleOpenReceive(request);
343
+ }
344
+
345
+ export const Route = createFileRoute("/openreceive/$")({
346
+ server: { handlers: { GET: openReceive, POST: openReceive } },
347
+ });
348
+ ```
349
+
350
+ ```ts
351
+ // src/lib/openreceive.server.ts
352
+ import { createStack } from "@openreceive/http";
353
+ import pg from "pg";
354
+ import { findOrder, currentVisitor } from "./orders.server"; // your own orders
355
+
356
+ export async function handleOpenReceive(request: Request): Promise<Response> {
357
+ // Read process.env inside the handler: on Workers it is empty at import.
358
+ const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 1 });
359
+ const stack = createStack({
360
+ wallet: { nwc: process.env.NWC_URI ?? "" },
361
+ storage: {
362
+ db: pool,
363
+ // Runs once, inside the transaction that records the payment.
364
+ onPaid: async ({ reference, paidAt, query }) => {
365
+ await query(
366
+ "UPDATE orders SET state = 'paid', paid_at = $1 WHERE id = $2 AND state = 'awaiting_payment'",
367
+ [paidAt, reference],
368
+ );
369
+ },
370
+ },
371
+ // The price comes from the order row, never from the request.
372
+ amountFor: async (reference) => {
373
+ const order = await findOrder(pool, reference);
374
+ return order?.state === "awaiting_payment"
375
+ ? { currency: order.currency, value: order.amount, description: order.title }
376
+ : null;
377
+ },
378
+ // Only the buyer may pay for their order.
379
+ authorize: async ({ request: incoming, resource }) => {
380
+ const order = resource.reference ? await findOrder(pool, resource.reference) : undefined;
381
+ const visitor = currentVisitor(incoming);
382
+ return Boolean(order && visitor && order.visitor === visitor);
383
+ },
384
+ // Cloudflare sets cf-connecting-ip on every request, and a client cannot.
385
+ // On Node behind your own proxy, read the header that proxy sets.
386
+ rateLimiting: {
387
+ ip: ({ request: incoming }) => incoming.headers.get("cf-connecting-ip") ?? undefined,
388
+ },
389
+ });
390
+ try {
391
+ return await stack.handler(request, { native: request });
392
+ } finally {
393
+ await stack.close();
394
+ await pool.end();
395
+ }
396
+ }
397
+ ```
398
+
399
+ `authorize` sees the incoming request with its cookies, so check it with
400
+ the session your app already has. The Next.js quickstart
401
+ explains each hook. They are the same here.
402
+
403
+ Create OpenReceive's two tables with your migrations, or run
404
+ `paymentsSchemaSql("postgres")` from `@openreceive/http` once. It is
405
+ idempotent. See Payment storage.
406
+
407
+ ### On Supabase (Lovable)
408
+
409
+ On Supabase the route keeps payments in your Supabase database through its
410
+ HTTPS API, with the project's secret key. There is no `pg` pool. Use
411
+ `@openreceive/*` 0.4.23 or newer:
412
+
413
+ ```sh
414
+ npm install @openreceive/http @openreceive/react
415
+ ```
416
+
417
+ 1. Apply the migration from `npx openreceive scaffold payments --supabase`.
418
+ It creates OpenReceive's tables, locked away from the browser's Supabase
419
+ key, and the functions that write them.
420
+ 2. Replace its placeholder `openreceive_on_paid` with the SQL that marks your
421
+ order paid. It runs inside the transaction that records the payment, for
422
+ the order's first payment only, so a failure there records nothing:
423
+
424
+ ```sql
425
+ create or replace function public.openreceive_on_paid(
426
+ p_reference text, p_payment_hash text, p_paid_at bigint
427
+ ) returns void language plpgsql as $$
428
+ begin
429
+ update public.orders set status = 'paid'
430
+ where id = p_reference::uuid and status = 'pending';
431
+ end
432
+ $$;
433
+ ```
434
+
435
+ 3. Write the route's server module:
436
+
437
+ ```ts
438
+ // src/lib/openreceive.server.ts
439
+ import { createStack } from "@openreceive/http";
440
+ import { findOrder, currentBuyer } from "./orders.server"; // your own orders
441
+
442
+ export async function handleOpenReceive(request: Request): Promise<Response> {
443
+ // Read process.env inside the handler: on Workers it is empty at import.
444
+ // Lovable sets SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY for server code.
445
+ const stack = createStack({
446
+ wallet: { nwc: process.env.NWC_URI ?? "" },
447
+ storage: {
448
+ supabase: {
449
+ url: process.env.SUPABASE_URL ?? "",
450
+ key: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
451
+ },
452
+ },
453
+ // The price comes from the order row, never from the request.
454
+ amountFor: async (reference) => {
455
+ const order = await findOrder(reference);
456
+ return order?.status === "pending"
457
+ ? { currency: order.currency, value: order.total, description: order.title }
458
+ : null;
459
+ },
460
+ // Only the buyer may pay for their order.
461
+ authorize: async ({ request: incoming, resource }) => {
462
+ const order = resource.reference ? await findOrder(resource.reference) : null;
463
+ const buyer = currentBuyer(incoming);
464
+ return Boolean(order && buyer && order.buyer_token === buyer);
465
+ },
466
+ rateLimiting: {
467
+ ip: ({ request: incoming }) => incoming.headers.get("cf-connecting-ip") ?? undefined,
468
+ },
469
+ });
470
+ try {
471
+ return await stack.handler(request, { native: request });
472
+ } finally {
473
+ await stack.close();
474
+ }
475
+ }
476
+ ```
477
+
478
+ There is no `onPaid`: `openreceive_on_paid` is the fulfillment. `findOrder`
479
+ reads the order with your server-side Supabase client; in a Lovable app that
480
+ is `supabaseAdmin` from `@/integrations/supabase/client.server`.
481
+
482
+ The checkout calls the payment routes from the browser with the page's
483
+ cookies, and nothing else. A Lovable app keeps its Supabase sign-in in the
484
+ browser, not in a cookie, so `authorize` cannot see who is signed in. Give
485
+ the buyer a cookie of their own instead: create orders in a server function
486
+ that sets an HttpOnly `buyer` cookie (a random value, kept across orders) and
487
+ stores the same value on the order as `buyer_token`. `currentBuyer` reads it
488
+ back from the request's `cookie` header.
489
+
490
+ Before it serves, the server checks the database, and the payment routes
491
+ answer 503 until the migration is applied and `openreceive_on_paid` is
492
+ yours; the log names the fix. More: Supabase over HTTPS.
493
+
494
+ ### The checkout page
495
+
496
+ ```tsx
497
+ // src/routes/checkout.$reference.tsx
498
+ import { Checkout } from "@openreceive/react";
499
+ import "@openreceive/react/styles.css";
500
+ import { createFileRoute } from "@tanstack/react-router";
501
+
502
+ export const Route = createFileRoute("/checkout/$reference")({
503
+ component: OrderCheckout,
504
+ });
505
+
506
+ function OrderCheckout() {
507
+ const { reference } = Route.useParams();
508
+ return <Checkout reference={reference} prefix="/openreceive" />;
509
+ }
510
+ ```
511
+
512
+ It renders on the server and starts the checkout in the browser. The payer
513
+ picks Bitcoin and gets the invoice.
514
+
515
+ ### Settings
516
+
517
+ - `NWC_URI`: a receive-only code from your wallet
518
+ ([get one](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
519
+ Optional: `LSC_URI_PRIMARY` for swaps
520
+ ([set one up](https://openreceive.org/set_up_swap_provider)).
521
+ - `DATABASE_URL`: your Postgres. On serverless hosts use the pooled URL. The
522
+ storage is tested through a transaction pooler.
523
+ - On Supabase over HTTPS, instead of `DATABASE_URL`: `SUPABASE_URL` and the
524
+ secret key in `SUPABASE_SERVICE_ROLE_KEY`. Lovable sets both itself.
525
+
526
+ Set them as server secrets, never with a `VITE_` prefix, which would put them
527
+ in the browser bundle.
528
+
529
+ No worker or cron job is needed. Each request to the payment routes also
530
+ checks the wallet for settled invoices, through a lock in your database. A
531
+ payer who closes the tab is settled on the next request.
@@ -3,7 +3,7 @@ This is the full file; follow it from Step 0.
3
3
  # OpenReceive agent directions (Next.js)
4
4
 
5
5
  ```sh
6
- # READ THIS FIRST: this file is 39 KB and a summary drops required steps. Download it whole:
6
+ # READ THIS FIRST: this file is 41 KB and a summary drops required steps. Download it whole:
7
7
  curl -fsSL https://openreceive.org/agent-directions/next/full.md
8
8
  # Skip the download only if you already have all of it: pasted, read from disk or fetched raw.
9
9
  ```
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/next/full.md
17
17
  - Do not suggest rotating or revoking a code because it was pasted here.
18
18
  - Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said "Bitcoin only".
19
19
 
20
- These directions describe OpenReceive 0.4.21.
20
+ These directions describe OpenReceive 0.4.23.
21
21
 
22
22
  Add OpenReceive to a Next.js App Router application — the app you are already
23
23
  working in. You do not need a copy of the OpenReceive source: the packages are
@@ -58,11 +58,12 @@ owns orders, users, prices, or fulfillment.
58
58
 
59
59
  ## Step 0 — ask for the two codes, one question at a time
60
60
 
61
- Before anything else, look at one file: the project's `.env.local`, if it exists.
62
- Read it only far enough to see whether `NWC_URI` and `LSC_URI_PRIMARY` are
63
- there and non-empty, and never print the values. A code that is already
64
- set is not asked for again; if both are set, skip to step 5
65
- (making the server load the file), then the quickstart.
61
+ Before anything else, check one file: the project's `.env.local`, if it exists.
62
+ `grep -E '^(NWC_URI|LSC_URI_PRIMARY)=.' .env.local | cut -d= -f1` names the codes
63
+ it sets without printing them. Check with that, here and after each write,
64
+ not by reading the file, and never print the values. A code that is already
65
+ set is not asked for again; if both are set, skip to step 5 (making the
66
+ server load the file), then the quickstart.
66
67
 
67
68
  On a hosted builder (v0, Vercel, Replit, Lovable), the user may say instead
68
69
  that both codes are already set as the project's environment variables or
@@ -98,14 +99,17 @@ you store it. Ask one question per message.
98
99
  Do not mention `.env`, exports, or "tell me when it's set".
99
100
  2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
100
101
  ask them to copy the receive-only code again. Otherwise write
101
- `NWC_URI=<paste>` into the project's `.env.local`, creating the file if needed.
102
- Make sure `.gitignore` covers `.env.local` (and `.dockerignore`, if the app has
103
- one). Never echo the value, commit it, or put it in client code. Reply only
104
- that it is saved, then ask the next question.
105
- 3. **Second message — swaps.** If the user already asked for stablecoins or
106
- altcoins, skip the yes/no and go straight to the walkthrough. Otherwise ask
107
- whether payers should also be able to pay with USDT, USDC, ETH or SOL. The
108
- walkthrough:
102
+ `NWC_URI=<paste>` into the project's `.env.local` with your file-editing
103
+ tool, creating the file if needed. Never write it with a shell command
104
+ (`echo`, `printf`, a heredoc, `python -c`): the command line shows the
105
+ code. Make sure `.gitignore` covers `.env.local` (and `.dockerignore`, if
106
+ the app has one). Never echo the value, commit it, or put it in client
107
+ code. Reply only that it is saved, then ask the next question.
108
+ 3. **Second message — swaps.** If the user asked for stablecoins, USDT, USDC,
109
+ ETH, SOL, altcoins or "crypto" (as in "Bitcoin and stablecoin payments"),
110
+ this message IS the walkthrough below: send it as it is, and do not ask yes
111
+ or no first. Otherwise ask whether payers should also be able to pay with
112
+ USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:
109
113
 
110
114
  > Go to https://lightning-swap.com, sign in for API keys, create a key, and
111
115
  > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
@@ -115,10 +119,10 @@ you store it. Ask one question per message.
115
119
  Mention FixedFloat only if they already use it.
116
120
  4. **When they paste it.** If it does not start with
117
121
  `lightning+swapconnect://`, ask them to copy it again. Otherwise add
118
- `LSC_URI_PRIMARY=<paste>` to the same `.env.local`, without echoing it. Swaps
119
- are now on, so build the refund route back (the swap non-negotiable below) as
120
- part of this integration. If they chose Bitcoin only, leave
121
- `LSC_URI_PRIMARY` unset and skip that route.
122
+ `LSC_URI_PRIMARY=<paste>` to the same `.env.local` with the file-editing
123
+ tool, never a shell command. Swaps are now on, so build the refund route
124
+ back (the swap non-negotiable below) as part of this integration. If they
125
+ chose Bitcoin only, leave `LSC_URI_PRIMARY` unset and skip that route.
122
126
  5. **Make the server load the file — yourself.** Next loads
123
127
  `.env.local` into `process.env` on its own: do not add `dotenv`, and never
124
128
  give a credential a `NEXT_PUBLIC_` prefix, which inlines it into the browser
@@ -153,23 +157,28 @@ Doctor reads `.env.local` itself. Never source it into a shell: the `&` in a
153
157
  code splits the value and prints the pieces.
154
158
 
155
159
  Give the user that checkout link. The browser check in the quickstart's step 6
156
- (payment-method icons, wallet logos, a pay tutorial) is theirs: tell them what
157
- to look at, and do not run it yourself.
160
+ (payment-method icons, wallet logos, a pay tutorial) is theirs: name it in one
161
+ line of your closing message, and do not run it yourself.
158
162
 
159
163
  You cannot pay the invoice: the code is receive-only. Do not pay, settle or
160
164
  mark an order paid, and do not look for a way to (a wallet control port, a
161
165
  test endpoint, another wallet). If the user wants a real settlement test, they
162
166
  pay on that link from their own wallet, and `onPaid` marks the order paid.
163
167
 
164
- Setup ends here. Say "Setup is finished" in one message of at most five
165
- lines, with the link and what to check. Do not list what changed, offer more
166
- work, or end the message on a question.
168
+ Setup ends here. Your last message starts "Setup is finished" and has at most
169
+ five short lines (about 80 characters each): the link, the methods it offers,
170
+ and one line of what to look at. Send nothing after it. Do not list what
171
+ changed, copy out the quickstart's browser checklist, offer more work, or end
172
+ the message on a question.
167
173
 
168
174
  - The link is to a real unpaid order. Keep that order; do not delete it.
175
+ - When this app's orders belong to a session or cookie, the user's browser
176
+ cannot open the order you made. Then the link is the shop's own page:
177
+ "Open <shop url> and click Buy: it opens the checkout." Do not explain order
178
+ ownership or mention your test order.
169
179
  - Name the methods it offers: Bitcoin, plus USDT, USDC, ETH and SOL when
170
- `LSC_URI_PRIMARY` is saved. Never say a coin will not work or will not be
171
- offered. Swap minimums apply per order, and a coin whose minimum is above an
172
- order's total says so on its own tile.
180
+ `LSC_URI_PRIMARY` is saved. Do not mention minimums, and never say a coin
181
+ will not work or will not be offered.
173
182
 
174
183
  ## Non-negotiables
175
184
 
@@ -186,6 +195,21 @@ itself, and they hold for every integration.
186
195
  - Work only in this application. Never read or run anything from another
187
196
  project on this machine (its `node_modules`, tools or source), for any
188
197
  reason. A browser and Playwright are not part of setup.
198
+ - Restart only this app's server, on the port it already uses: stop the
199
+ process you started, or the one listening on that port, by its pid (or
200
+ restart its Compose service). Never `pkill` or `killall` by name: that
201
+ stops other people's servers too. Start it the way this project already
202
+ does (its README, Procfile, compose file or package script), on its own
203
+ port, not a port you pick.
204
+ - Run commands where the app runs. When a compose file builds it, run its
205
+ package manager, generators, migrations and doctor inside that service
206
+ (`docker compose exec` or `run`), and rebuild the image after adding a
207
+ package. This machine's language version and database path are not the
208
+ app's.
209
+ - To check that the running app sees the codes, run doctor: it reports each
210
+ one as present or missing and never prints a value. Never print the
211
+ environment (`printenv`, `env`, `docker compose config`), even filtered to
212
+ names.
189
213
  - The host owns the price. `amountFor` reads it from your own data; reject
190
214
  payer-supplied amounts.
191
215
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM