openreceive 0.4.11 → 0.4.14

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.
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Laravel)
2
2
 
3
- These directions describe OpenReceive 0.4.11.
3
+ These directions describe OpenReceive 0.4.14.
4
4
 
5
5
  Add OpenReceive to a Laravel application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the package is on Packagist
@@ -35,71 +35,94 @@ The one required credential is a receive-only NWC code (Nostr Wallet Connect):
35
35
  a string from the merchant's wallet that can create invoices and read their
36
36
  status, and cannot spend. A swap provider (an "LSC" code) optionally lets the
37
37
  payer send USDT, USDC, ETH or SOL instead, converted into that same
38
- Lightning payment. You supply those credentials and three hooks — `authorize`,
38
+ Lightning payment. Step 0 collects those credentials from the user; you write three hooks — `authorize`,
39
39
  `amountFor`, `onPaid` on one `App\OpenReceive\Host` class;
40
40
  OpenReceive supplies invoices, polling, settlement and the checkout UI. It never
41
41
  owns orders, users, prices, or fulfillment.
42
42
 
43
- ## Step 0 — check the environment before you write code
44
-
45
- Do this before installing the package or editing files.
46
-
47
- 1. Look for `NWC_URI` in this app's server environment — `.env`, the deploy
48
- config, Forge/Vapor/Envoyer environment panels, whatever this app already
49
- uses. If the app runs in a container the value is in none of those: ask the
50
- running process (`docker exec <container> printenv NWC_URI`), because finding
51
- the NAME in a compose file or `.env.example` proves nothing about the value.
52
- Never print or echo the value itself; only report whether it is set. Check
53
- for `LSC_URI_PRIMARY` in the same pass. Remember `php artisan config:cache`:
54
- a cached config captured the values at cache time, and editing `.env`
55
- afterwards changes nothing until the cache is rebuilt.
56
-
57
- If OpenReceive is already installed here, `php artisan openreceive:doctor`
58
- answers this whole step in one command — every credential as set/unset, the
59
- host class and which hooks are still placeholders, the route mount, and the
60
- wallet preflight. It never prints a value.
61
- 2. If BOTH are already set — the common case in an existing app — say so and go
62
- straight to the quickstart. Steps 3 and 4 are for an environment that is
63
- missing one; do not stop to ask about altcoins that are already configured.
64
- If only `NWC_URI` is set, Bitcoin already works: continue, and raise the
65
- altcoin question at step 4 rather than blocking on it.
66
- 3. If `NWC_URI` is missing or empty, stop and tell the user exactly what to
67
- create:
68
-
69
- > OpenReceive cannot issue an invoice without a receive-only NWC code. Get
70
- > one at https://openreceive.org/get_a_nwc_code_to_receive_payments, then
71
- > put `NWC_URI=<the code>` in this app's server environment — for most apps
72
- > that is a `.env` file in the project root — and tell me when it's set.
73
-
74
- Wait for the user before wiring OpenReceive; do not invent a placeholder
75
- value. Waiting is not idleness: you may write `.env.example` with the
76
- variable NAMES only (`NWC_URI=`, `LSC_URI_PRIMARY=`) so the merchant has a
77
- file to copy, and keep building the parts of the host that do not touch
78
- OpenReceive — the order model, the cart, the routes. The stop guards the
79
- credential, not the rest of the app.
80
- 4. If `LSC_URI_PRIMARY` was not already set, ask the user: "Do you want to
81
- accept altcoins and stablecoins (USDT, USDC, ETH, SOL) as well as
82
- Bitcoin?"
83
-
84
- - Yes → send them to https://openreceive.org/set_up_swap_provider for a
85
- swap-provider (LSC) code, to set as `LSC_URI_PRIMARY` in the same server
86
- environment. Do NOT wait for it: no application code reads the value, so
87
- the integration is identical with or without it — the engine picks it up
88
- from the environment and swaps switch on. What a yes DOES change is the
89
- refund route back (the swap non-negotiable below): build it as part of
90
- this integration, not when the code arrives.
91
- - No → skip it. Bitcoin over Lightning works with `NWC_URI` alone, and you
92
- can add a swap provider later without changing application code.
93
- 5. Check the environment again and confirm `NWC_URI` is present.
94
- `LSC_URI_PRIMARY` may land later; swaps stay off until it does, and no code
95
- changes when it arrives.
96
- 6. If OpenReceive is ALREADY installed here, check the installed versions of
97
- `openreceive/laravel` (`composer show openreceive/laravel`) and
98
- `@openreceive/browser` against the release named at the top of this file.
99
- The headless display models below do not exist in older versions, and the
100
- first tile click throws with nothing saying why. Upgrade first — and if this
101
- app runs in containers, rebuild the images: `vendor/` is baked into the
102
- image, so an in-place `composer update` is undone by the next `compose up`.
43
+ ## Step 0 — ask for the two codes, one question at a time
44
+
45
+ Before anything else, look at one file: the project's `.env`, if it exists.
46
+ Read it only far enough to see whether `NWC_URI` and `LSC_URI_PRIMARY` are
47
+ there and non-empty, and never print the values. A code that is already
48
+ set is not asked for again; if both are set, skip to step 5
49
+ (making the server load the file), then the quickstart.
50
+
51
+ Otherwise your next action is a question to the user. Do not install the package,
52
+ edit the app, write `.env.example`, or search anywhere else before asking
53
+ it. Do not read deploy config (compose files, Forge/Vapor/Envoyer panels), Docker containers, or process environments, and never run
54
+ `printenv`. A new shop has neither code yet: hunting for one burns the turn and
55
+ can leak other secrets into the transcript.
56
+
57
+ Two server-only URIs are needed before the quickstart:
58
+
59
+ - `NWC_URI` — a receive-only Nostr Wallet Connect code,
60
+ `nostr+walletconnect://…`. Required for Bitcoin.
61
+ - `LSC_URI_PRIMARY` — a Lightning Swap Connect URI,
62
+ `lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
63
+ only when the user says they want Bitcoin alone.
64
+
65
+ The user never edits an environment file. They paste each code into the chat;
66
+ you store it. Ask one question per message.
67
+
68
+ 1. **First message — the NWC code, and nothing else.** Ask for it and walk them
69
+ through getting it:
70
+
71
+ > To receive payments I need a receive-only wallet code. In Rizful: open
72
+ > the menu, tap NWC, choose Receive-only NWC code, and tap Copy
73
+ > (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
74
+ > rather run your own wallet, Alby Hub works too: Connections → Add
75
+ > Connection → Read Only. Paste the code here and I will store it.
76
+
77
+ Do not mention `.env`, exports, or "tell me when it's set".
78
+ 2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
79
+ ask them to copy the receive-only code again. Otherwise write
80
+ `NWC_URI=<paste>` into the project's `.env`, creating the file if needed.
81
+ Make sure `.gitignore` covers `.env` (and `.dockerignore`, if the app has
82
+ one). Never echo the value, commit it, or put it in client code. Reply only
83
+ that it is saved, then ask the next question.
84
+ 3. **Second message — swaps.** If the user already asked for stablecoins or
85
+ altcoins, skip the yes/no and go straight to the walkthrough. Otherwise ask
86
+ whether payers should also be able to pay with USDT, USDC, ETH or SOL. The
87
+ walkthrough:
88
+
89
+ > Go to https://lightning-swap.com, sign in for API keys, create a key, and
90
+ > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
91
+ > it here and I will store it — or say "Bitcoin only" and I will continue
92
+ > without it.
93
+
94
+ Mention FixedFloat only if they already use it.
95
+ 4. **When they paste it.** If it does not start with
96
+ `lightning+swapconnect://`, ask them to copy it again. Otherwise add
97
+ `LSC_URI_PRIMARY=<paste>` to the same `.env`, without echoing it. Swaps
98
+ are now on, so build the refund route back (the swap non-negotiable below) as
99
+ part of this integration. If they chose Bitcoin only, leave
100
+ `LSC_URI_PRIMARY` unset and skip that route.
101
+ 5. **Make the server load the file — yourself.** Laravel loads
102
+ `.env` itself. If the app runs `php artisan config:cache`, run
103
+ `php artisan config:clear` (or re-cache) after writing the file: a cached
104
+ config never reads `.env` again. When the app is started with Docker
105
+ Compose, give the service `env_file: .env`. Restart the server after
106
+ writing the file.
107
+
108
+ Do not invent placeholder URIs. Start the quickstart only once `NWC_URI` is
109
+ saved and `LSC_URI_PRIMARY` is saved or explicitly declined. The first boot
110
+ runs the receive-only preflight: if it reports spend methods such as
111
+ `pay_invoice`, remove `NWC_URI` from `.env` and ask for a receive-only code
112
+ again. Never set the spend-capable override to get past it.
113
+
114
+ ### Upgrading an existing install
115
+
116
+ This is not the opening move of a new integration. When OpenReceive is ALREADY
117
+ installed here, `php artisan openreceive:doctor` reports every credential as
118
+ set/unset (never the value), the host class and any placeholder hooks, the
119
+ route mount, and the wallet preflight. Check the installed
120
+ `openreceive/laravel` (`composer show openreceive/laravel`) and
121
+ `@openreceive/browser` against the release named at the top of this file: the
122
+ headless display models below do not exist in older versions, and the first
123
+ tile click throws with nothing saying why. Upgrade first — and if this app runs
124
+ in containers, rebuild the images: `vendor/` is baked into the image, so an
125
+ in-place `composer update` is undone by the next `compose up`.
103
126
 
104
127
  Only then start the quickstart.
105
128
 
@@ -135,15 +158,19 @@ itself, and they hold for every integration.
135
158
  late becomes `refund_required`, and the payer claims it on a SECOND VISIT,
136
159
  after leaving your page to fetch an address from another wallet. Three things
137
160
  must exist or that money is unreachable through your UI: a per-order URL your
138
- server serves (`/checkout/:reference` — `syncUrl` on the drop-ins), your own
139
- order-summary route to restore the order from, and the ATTEMPT.
161
+ server serves (`/checkout/:reference` — `syncUrl` on `<Checkout>`, `sync-url`
162
+ or `resumable` on `<openreceive-checkout>`), your own order-summary route to
163
+ restore the order from, and the ATTEMPT.
140
164
  `/checkouts/prepare` returns no attempts, so a checkout rebuilt from the
141
165
  reference alone opens on the method grid. Re-picking the same coin
142
166
  (`POST /swaps`) re-serves the committed attempt — but only while it is live,
143
167
  and the shadow invoice behind a swap lasts about half an hour, after which the
144
168
  same click mints a NEW deposit address and the refund is off-screen. Keep the
145
169
  `payment_hash` and reopen the attempt with `POST /swaps/status`, which has no
146
- such window. https://openreceive.org/guides/swap-refunds.md
170
+ such window. On the drop-ins: `resumePaymentHash`, fed from `onState`, on
171
+ `<Checkout>`; the `resume-payment-hash` attribute, fed from the
172
+ `openreceive-state` event (`event.detail.state.payment_hash`), on
173
+ `<openreceive-checkout>`. https://openreceive.org/guides/swap-refunds.md
147
174
  - Show the payer WHAT THEY ARE BUYING. Return an optional `description` beside
148
175
  the price from `amountFor` and both drop-ins render it above the
149
176
  amount. Without it the checkout is a QR and "$1.00" with no sign of what the
@@ -302,12 +329,18 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-lara
302
329
 
303
330
  ## Laravel quickstart
304
331
 
305
- Requires PHP ≥ 8.2 (64-bit) and Laravel ≥ 11 (12 current). Extensions:
306
- `ext-gmp` is REQUIRED — the NWC transport's elliptic-curve math depends on it
307
- — plus `sodium`, `mbstring` and the `pdo_*` driver for your database
308
- (`pdo_pgsql`, `pdo_mysql` or `pdo_sqlite`). `php -m` lists what your build
309
- has; on Debian/Ubuntu it is `apt-get install php8.2-gmp`, in the official
310
- Docker image `docker-php-ext-install gmp`.
332
+ Requires PHP ≥ 8.2 (64-bit) and Laravel ≥ 11 (12 current). You also need these
333
+ PHP extensions:
334
+
335
+ - `ext-gmp`, which is REQUIRED. The NWC transport's elliptic-curve math depends
336
+ on it.
337
+ - `sodium` and `mbstring`.
338
+ - The `pdo_*` driver for your database (`pdo_pgsql`, `pdo_mysql` or
339
+ `pdo_sqlite`).
340
+
341
+ `php -m` lists what your build has. To add gmp on Debian/Ubuntu, run
342
+ `apt-get install php8.2-gmp`. In the official Docker image, run
343
+ `docker-php-ext-install gmp`.
311
344
 
312
345
  Add the Laravel package:
313
346
 
@@ -315,10 +348,10 @@ Add the Laravel package:
315
348
  composer require openreceive/laravel
316
349
  ```
317
350
 
318
- That is the whole install: `openreceive/laravel` depends on
319
- `openreceive/openreceive`, the engine, so the default wallet client — built from
320
- `NWC_URI` — works with nothing else added, and package discovery registers the
321
- service provider. Hosts that bring their own NWC client bind
351
+ That is the whole install. `openreceive/laravel` depends on
352
+ `openreceive/openreceive`, the engine. So the default wallet client works with
353
+ nothing else added. It is built from `NWC_URI`. Package discovery registers the
354
+ service provider. If your app brings its own NWC client, bind
322
355
  `OpenReceive\Nwc\ReceiveNwcClient` in the container instead.
323
356
 
324
357
  Then run:
@@ -330,35 +363,36 @@ php artisan migrate
330
363
 
331
364
  `openreceive:install` writes three files:
332
365
 
333
- - `config/openreceive.php` — the settings. The host hook is a CLASS NAME, not
334
- a closure, because `php artisan config:cache` serializes this file and a
335
- closure anywhere in it fails the cache;
336
- - `app/OpenReceive/Host.php` — the three hooks (`authorize`, `amountFor`,
337
- `onPaid`), with the two generated placeholders wired and the fulfillment
338
- note as comments;
339
- - `database/migrations/*_create_openreceive_tables.php` — one migration for
340
- both engine tables (`openreceive_payments` and `openreceive_meta`). Its DDL
341
- comes from the engine's `PaymentsSchema::statements()` for your connection's
342
- driver — PostgreSQL, MySQL/MariaDB and SQLite — and `php artisan migrate` runs
343
- it like any migration of your own; there is no second runner.
366
+ - `config/openreceive.php`: the settings. The host hook is a CLASS NAME, not a
367
+ closure. `php artisan config:cache` serializes this file, and a closure
368
+ anywhere in it makes the cache fail.
369
+ - `app/OpenReceive/Host.php`: the three hooks (`authorize`, `amountFor`,
370
+ `onPaid`), with the two generated placeholders wired in and the fulfillment
371
+ note as comments.
372
+ - `database/migrations/*_create_openreceive_tables.php`: one migration for both
373
+ engine tables (`openreceive_payments` and `openreceive_meta`). Its DDL comes
374
+ from the engine's `PaymentsSchema::statements()` for your connection's
375
+ driver: PostgreSQL, MySQL/MariaDB or SQLite. `php artisan migrate` runs it
376
+ like any of your own migrations. There is no second runner.
344
377
 
345
378
  → [OpenReceive\Storage](https://openreceive.org/guides/api-reference.md#openreceivestorage)
346
379
 
347
380
  There is no Eloquent model for the engine's tables, and you do not write one.
348
381
  The engine owns the table's commit locking, write-once settlement, and
349
- reconciliation state machine. `reference` is indexed but not unique (a
350
- reference may have many historical attempts); `payment_hash` is globally unique.
382
+ reconciliation state machine. `reference` is indexed but not unique, because
383
+ one reference may have many historical attempts. `payment_hash` is globally
384
+ unique.
351
385
 
352
386
  #### Fulfill exactly once
353
387
 
354
388
  Within OpenReceive's own settlement paths, `onPaid` runs at most once per
355
- reference: a second payment to a second invoice is recorded with
389
+ reference. A second payment to a second invoice is recorded with
356
390
  `status_reason = "duplicate_settlement"` and never fulfills again.
357
391
 
358
- The one thing you own: **if anything other than OpenReceive can also fulfill
359
- an order** — an admin action, a second payment processor, a replayed job —
360
- those paths race each other, and `onPaid` must be idempotent. The generated
361
- `Host.php` spells this out and shows the guarded transition:
392
+ One case is yours to handle. **If anything other than OpenReceive can also
393
+ fulfill an order**, such as an admin action, a second payment processor, or a
394
+ replayed job, those paths race each other. Then `onPaid` must be idempotent.
395
+ The generated `Host.php` explains this and shows the guarded transition:
362
396
 
363
397
  ```php
364
398
  public function onPaid(PaymentSettlement $settlement): void
@@ -374,23 +408,28 @@ public function onPaid(PaymentSettlement $settlement): void
374
408
  }
375
409
  ```
376
410
 
377
- Delivery is at-least-once: `onPaid` runs inside the settlement transaction,
378
- and an exception rolls it back for the next pass to retry. Keep it to database
379
- writes on the order — an email or webhook sent from here would survive the
380
- rollback and go out again. The `'paid'` transition above is the flag; let your
381
- own job drain it after commit, or implement `OpenReceive\Hosts\AfterPaid`:
382
- its `afterPaid(PaymentSettlement $settlement)` runs after COMMIT, best-effort,
383
- for the same first settled attempt — the place for `FulfillOrder::dispatch(…)`
384
- or an event. And open no `DB::transaction()` of your own inside `onPaid`: the
385
- engine already holds the transaction on this connection, and PDO refuses a
386
- nested `BEGIN`.
387
-
388
- **A query-builder `update()` fires no model events.** That is the point — it is
389
- one conditional `UPDATE`, so the claim is atomic and there is no model code
411
+ Delivery is at-least-once. `onPaid` runs inside the settlement transaction. If
412
+ it throws, the transaction rolls back and the next pass retries. So keep
413
+ `onPaid` to database writes on the order. An email or webhook sent from here
414
+ would survive the rollback and go out again. The `'paid'` transition above is
415
+ the flag. Drain it after commit in one of two ways:
416
+
417
+ - Let your own job drain it.
418
+ - Implement `OpenReceive\Hosts\AfterPaid`. Its
419
+ `afterPaid(PaymentSettlement $settlement)` runs after COMMIT, best-effort,
420
+ for the same first settled attempt. Put `FulfillOrder::dispatch(…)` or an
421
+ event there.
422
+
423
+ Do not open a `DB::transaction()` of your own inside `onPaid`. The engine
424
+ already holds the transaction on this connection, and PDO refuses a nested
425
+ `BEGIN`.
426
+
427
+ **A query-builder `update()` fires no model events.** That is intended. It
428
+ runs one conditional `UPDATE`, so the claim is atomic and no model code runs
390
429
  between the check and the write. It also means no observer, no `saved` event
391
- and no `Model::updated` listener runs, which is fine for a job draining the
392
- flag and useless for a model that owns the transition through events. If yours
393
- does, take a row lock for the duration instead:
430
+ and no `Model::updated` listener runs. That is fine for a job that drains the
431
+ flag. It does not work for a model that owns the transition through events. If
432
+ yours does, take a row lock for the duration instead:
394
433
 
395
434
  ```php
396
435
  public function onPaid(PaymentSettlement $settlement): void
@@ -403,26 +442,29 @@ public function onPaid(PaymentSettlement $settlement): void
403
442
  }
404
443
  ```
405
444
 
406
- **Unlocking a download works the same way.** If what the payer bought is a
407
- file, do not unlock it in the browser: gate the download route on the paid
408
- order row — `Order::where('id', $id)->where('user_id', $request->user()->id)->where('state', 'paid')->firstOrFail()`
409
- or a 404 — and serve the file only then. The `'paid'` written above is the
410
- unlock; the client never decides an order was fulfilled, it re-reads the row.
411
- Buy a Button's `ShopController::download` is this in twenty lines.
412
-
413
- Both shapes are idempotent, and both are correct. They differ only in whether
414
- your model layer gets to run: the query-builder `update()` skips it and is the
415
- right default; the row lock holds the row for the duration of the method and
416
- is what you want when the transition has to go through your model. The
417
- generated fulfillment note says the same thing — if your fulfillment is a
418
- read-modify-write that cannot be expressed as one conditional `UPDATE`, take
419
- the lock.
420
-
421
- A runnable illustration of this boundary — not a template to copy models from —
422
- is Buy a Button
423
- (`examples/buttons/server/laravel`).
424
- It has products, visitors, and orders, with the three hooks as the only bridge.
425
- Map that shape onto the models in THIS app.
445
+ **Unlocking a download works the same way.** If the payer bought a file, do not
446
+ unlock it in the browser. Gate the download route on the paid order row, and
447
+ serve the file only if that row exists:
448
+ `Order::where('id', $id)->where('user_id', $request->user()->id)->where('state', 'paid')->firstOrFail()`,
449
+ or a 404 otherwise. The `'paid'` written above is the unlock. The client never
450
+ decides that an order was fulfilled. It re-reads the row. Buy a Button's
451
+ `ShopController::download` does this in twenty lines.
452
+
453
+ Both shapes are idempotent and correct. They differ only in whether your model
454
+ layer runs:
455
+
456
+ - The query-builder `update()` skips the model layer. It is the right default.
457
+ - The row lock holds the row for the duration of the method. Use it when the
458
+ transition has to go through your model.
459
+
460
+ The generated fulfillment note says the same thing. If your fulfillment is a
461
+ read-modify-write that one conditional `UPDATE` cannot express, take the lock.
462
+
463
+ Buy a Button
464
+ (`examples/buttons/server/laravel`)
465
+ is a runnable illustration of this boundary. It is not a template to copy
466
+ models from. It has products, visitors, and orders, and the three hooks are the
467
+ only bridge. Map that shape onto the models in THIS app.
426
468
 
427
469
  ### Add wallet credentials
428
470
 
@@ -436,31 +478,37 @@ LSC_URI_BACKUP=
436
478
  ```
437
479
 
438
480
  1. Get a receive-only NWC code from a compatible wallet
439
- ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
440
- → `NWC_URI`.
441
- 2. Optionally set up a [swap provider](https://openreceive.org/set_up_swap_provider)
442
- → `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP` if you have one).
443
-
444
- Never put these values in browser code. Your application refuses to start if
445
- the NWC code also advertises spend methods such as `pay_invoice`; mint a
446
- receive-only code ([Security](https://openreceive.org/guides/security.md)).
447
-
448
- Under `php artisan config:cache` Laravel never reads `.env` at runtime — the
449
- values are captured when you cache, as for every other Laravel secret — so
481
+ ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
482
+ Put it in `NWC_URI`.
483
+ 2. Optional: set up a [swap provider](https://openreceive.org/set_up_swap_provider).
484
+ Put its connection string in `LSC_URI_PRIMARY`, and a second one in
485
+ `LSC_URI_BACKUP` if you have one.
486
+
487
+ Never put these values in browser code. Your app refuses to start if the NWC
488
+ code also advertises spend methods such as `pay_invoice`. Create a
489
+ receive-only code instead ([Security](https://openreceive.org/guides/security.md)).
490
+
491
+ Under `php artisan config:cache`, Laravel never reads `.env` at runtime. The
492
+ values are captured when you cache, as with every other Laravel secret. So
450
493
  re-run `config:cache` after changing one. The engine redacts connection
451
- strings in every log line, and `php artisan openreceive:doctor` prints each
452
- variable as set/unset only.
494
+ strings in every log line. `php artisan openreceive:doctor` prints each
495
+ variable only as set or unset.
453
496
  → [Environment variables](https://openreceive.org/guides/environment-variables.md).
454
497
 
455
498
  ### Configure the host hooks
456
499
 
457
500
  `app/OpenReceive/Host.php` needs three things: authorization, the trusted
458
- price, and fulfillment. All three receive the `reference` — a string you
459
- choose, and the fulfillment identity: your order id, one per thing you
460
- fulfill, created before checkout, kept across retries, never reused.
461
- OpenReceive never looks inside it, but `onPaid` commits fulfillment once per reference, a new
462
- checkout under a reference that already settled is refused with 409, and a
463
- fresh id per page load lets one order be paid twice.
501
+ price, and fulfillment. All three receive the `reference`. This is a string you
502
+ choose, and it is the fulfillment identity. Use your order id:
503
+
504
+ - one per thing you fulfill,
505
+ - created before checkout,
506
+ - kept across retries,
507
+ - never reused.
508
+
509
+ OpenReceive never looks inside it. But `onPaid` commits fulfillment once per
510
+ reference, and a new checkout under a reference that already settled is
511
+ refused with 409. A fresh id per page load would let one order be paid twice.
464
512
 
465
513
  ```php
466
514
  <?php
@@ -521,67 +569,74 @@ final class Host implements OpenReceiveHost
521
569
  }
522
570
  ```
523
571
 
524
- `config/openreceive.php` names that class (`'host' => App\OpenReceive\Host::class`)
525
- and the provider resolves it from the container, so it may take constructor
526
- dependencies. `onPaid` runs inside the settlement transaction, only for the
527
- first settled attempt for a reference.
572
+ `config/openreceive.php` names that class
573
+ (`'host' => App\OpenReceive\Host::class`). The provider resolves it from the
574
+ container, so it may take constructor dependencies. `onPaid` runs inside the
575
+ settlement transaction, only for the first settled attempt for a reference.
528
576
  → [OpenReceive\Host](https://openreceive.org/guides/api-reference.md#openreceivehost) ·
529
577
  [the authorize context](https://openreceive.org/guides/api-reference.md#the-authorize-context-php)
530
578
 
531
- The routes mount under `config('openreceive.route_prefix')` (`openreceive`) and
532
- `config('openreceive.middleware')` (`['web']`). The `web` group is how the
533
- engine picks up your application's session and CSRF protection: Laravel's
534
- `VerifyCsrfToken` reads `X-CSRF-TOKEN`, and the checkout client sends it from
535
- `<meta name="csrf-token" content="{{ csrf_token() }}">` automatically — keep
536
- that tag in the layout that renders the checkout. The same group brings every
537
- middleware it carries; a global `auth` redirect on the `web` group would send
538
- the engine's JSON routes to a login page too, so keep such guards on your own
539
- route groups, not on `web` itself.
540
-
541
- The generated `Host.php` ships `use LoggingOnPaid;` — a placeholder that only
542
- logs the settlement and fulfills nothing. Replace it with your real fulfillment
543
- (as above); the engine warns every time your application boots while the
544
- placeholder is still there, because orders would otherwise be recorded as
545
- settled without ever being fulfilled. The same applies to `use
546
- AllowAllAuthorize;`, the generated allow-all placeholder: it treats
547
- possession of the reference as authorization, which is safe only while
548
- references are unguessable, and the engine warns at boot until you replace it
549
- with your own ownership check (as above). Replace both, not just `onPaid`.
550
-
551
- The amount always comes from your own order record; payer-supplied amounts are
579
+ The routes mount under `config('openreceive.route_prefix')` (`openreceive`),
580
+ with the middleware in `config('openreceive.middleware')` (`['web']`). The
581
+ `web` group is how the engine picks up your application's session and CSRF
582
+ protection. Laravel's `VerifyCsrfToken` reads `X-CSRF-TOKEN`. The checkout
583
+ client sends it automatically from
584
+ `<meta name="csrf-token" content="{{ csrf_token() }}">`, so keep that tag in
585
+ the layout that renders the checkout.
586
+
587
+ The `web` group also brings every middleware it carries. A global `auth`
588
+ redirect on the `web` group would send the engine's JSON routes to a login page
589
+ too. Keep such guards on your own route groups, not on `web` itself.
590
+
591
+ The generated `Host.php` ships two placeholders. Replace both, not just
592
+ `onPaid`:
593
+
594
+ - `use LoggingOnPaid;` only logs the settlement and fulfills nothing. Replace
595
+ it with your real fulfillment (as above). Until you do, orders would be
596
+ recorded as settled without ever being fulfilled, so the engine warns every
597
+ time your application boots.
598
+ - `use AllowAllAuthorize;` allows everything. It treats possession of the
599
+ reference as authorization, which is safe only while references are
600
+ unguessable. The engine warns at boot until you replace it with your own
601
+ ownership check (as above).
602
+
603
+ The amount always comes from your own order record. Payer-supplied amounts are
552
604
  rejected. The engine's `PaymentRepository` interface remains the escape hatch
553
- for custom-storage applications and is not part of the quickstart.
554
-
555
- For public web shops, opt into the per-IP invoice cap with
556
- `'rate_limiting' => true`; leave it off (the default) when many payers share
557
- one IP. The client IP is `$request->ip()`, so Laravel's `TrustProxies` decides
558
- who the payer is behind a proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
559
-
560
- In production the engine builds the wallet client — and runs its receive-only
561
- preflight — eagerly when your application boots, so a missing `NWC_URI`, a
562
- dead relay, or a spend-capable wallet stops the deploy instead of surfacing as
563
- customer-facing 500s on the first checkout. Outside production (tests,
564
- consoles) the client is built lazily so no live wallet is needed.
565
-
566
- PHP builds the whole engine again on every request, so that same preflight
567
- would run per request under PHP-FPM or Apache; the package remembers the
568
- wallet's info event in your default cache store for
569
- `config('openreceive.wallet_info_cache_seconds')` (600) so a checkout request
570
- costs one relay round trip, not two.
605
+ for apps with custom storage. It is not part of the quickstart.
606
+
607
+ For public web shops, turn on the per-IP invoice cap with
608
+ `'rate_limiting' => true`. Leave it off (the default) when many payers share
609
+ one IP. The client IP is `$request->ip()`, so behind a proxy Laravel's
610
+ `TrustProxies` decides who the payer is. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
611
+
612
+ In production, the engine builds the wallet client when your app boots. It
613
+ also runs the receive-only preflight right away: it reaches the wallet and
614
+ checks that the code cannot spend. A missing `NWC_URI`, a dead relay, or a
615
+ spend-capable wallet then stops the deploy. Otherwise those problems would
616
+ show up as 500 errors for customers on the first checkout. Outside production
617
+ (tests, consoles), the engine builds the client lazily, on first use, so no
618
+ live wallet is needed.
619
+
620
+ PHP builds the whole engine again on every request. Under PHP-FPM or Apache,
621
+ that same preflight would then run per request. To avoid that, the package
622
+ caches the wallet's info event in your default cache store for
623
+ `config('openreceive.wallet_info_cache_seconds')` (600). A checkout request
624
+ then costs one relay round trip, not two.
571
625
 
572
626
  ### Render the checkout
573
627
 
574
- Serve the compiled `styles.css` without Tailwind processing: import it from
575
- JavaScript (with a CSS-capable bundler) or use a plain `<link rel="stylesheet">`.
576
- Do not `@import` it into the host Tailwind entry. Its zero-specificity rules
577
- allow host styles to override checkout styles; scoping does not prevent that.
628
+ Serve the compiled `styles.css` without Tailwind processing. Either import it
629
+ from JavaScript (with a CSS-capable bundler) or use a plain
630
+ `<link rel="stylesheet">`. Do not `@import` it into your Tailwind entry. Its
631
+ rules have zero specificity, so your own styles can override checkout styles.
632
+ Scoping does not prevent that.
578
633
 
579
- The engine serves JSON checkout routes only — rendering is your view. Any
580
- OpenReceive frontend package works against the `/openreceive` mount; the
581
- smallest is the custom element (its default `prefix` is already
582
- `/openreceive`, and the package ships a self-contained `styles.css`; it is
583
- scoped to what OpenReceive renders). Laravel ships Vite, so the package installs like any
584
- other frontend dependency:
634
+ The engine serves JSON checkout routes only. Your view does the rendering. Any
635
+ OpenReceive frontend package works against the `/openreceive` mount. The
636
+ smallest is the custom element. Its default `prefix` is already
637
+ `/openreceive`. The package ships a self-contained `styles.css`, scoped to what
638
+ OpenReceive renders. Laravel ships Vite, so the package installs like any other
639
+ frontend dependency:
585
640
 
586
641
  ```sh
587
642
  npm install @openreceive/elements
@@ -605,32 +660,32 @@ defineElements();
605
660
  ```
606
661
 
607
662
  The element creates the checkout for `reference`, then renders and polls
608
- itself. React/Vue/Svelte/Angular apps use the matching wrapper package instead
609
- — same props and defaults ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a
610
- custom checkout only if this app cannot use a drop-in; then
611
- `@openreceive/browser/headless` is the API
612
- ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
663
+ itself. React, Vue, Svelte, and Angular apps use the matching wrapper package
664
+ instead, with the same props and defaults
665
+ ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a custom checkout only if
666
+ this app cannot use a drop-in. In that case `@openreceive/browser/headless` is
667
+ the API ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
613
668
 
614
669
  Everything the checkout draws ships inside the JavaScript: the payment-method
615
670
  icons, the wallet logos and the pay tutorials. There is no image file to copy
616
671
  or serve and no asset option to set. Deploy your normal JavaScript and CSS
617
672
  build output, including any generated JavaScript chunks. Bundlers with code
618
- splitting can defer tutorial screenshots until first open; single-file builds
619
- (including the standalone checkout) include them upfront. If your
620
- Content-Security-Policy has a strict `img-src`, allow `data:`
673
+ splitting can load tutorial screenshots only when a tutorial is first opened.
674
+ Single-file builds, including the standalone checkout, include them upfront. If
675
+ your Content-Security-Policy has a strict `img-src`, allow `data:`
621
676
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
622
677
 
623
- Then open the checkout in a browser, confirm the payment-method icons and
678
+ Then open the checkout in a browser. Confirm the payment-method icons and
624
679
  wallet logos render, and open a wallet's pay tutorial to check its screenshots.
625
- If an image is missing, inspect the console for CSP violations and the Network
680
+ If an image is missing, check the console for CSP violations and the Network
626
681
  panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
627
682
  complete build output. Do not add image routes, copy package source images, or
628
683
  use registry `icon_path` / tutorial `path` keys as browser URLs.
629
684
 
630
685
  **No npm in this project?** Every GitHub release attaches
631
- `standalone-checkout-<version>.tar.gz`: one self-registering ESM file, its
632
- stylesheet and a `MANIFEST.json` of hashes. Unpack it into
633
- `public/openreceive/` and the same element needs two tags:
686
+ `standalone-checkout-<version>.tar.gz`. It holds one self-registering ESM file,
687
+ its stylesheet and a `MANIFEST.json` of hashes. Unpack it into
688
+ `public/openreceive/`, and the same element needs just two tags:
634
689
 
635
690
  ```blade
636
691
  <link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
@@ -639,7 +694,7 @@ stylesheet and a `MANIFEST.json` of hashes. Unpack it into
639
694
  <openreceive-checkout reference="{{ $order->id }}"></openreceive-checkout>
640
695
  ```
641
696
 
642
- `MANIFEST.json` carries the version and a SHA-256 per file; keep the tarball
697
+ `MANIFEST.json` carries the version and a SHA-256 per file. Keep the tarball
643
698
  version in step with the Composer package.
644
699
 
645
700
  ### Reconciliation
@@ -655,33 +710,40 @@ load:
655
710
  php artisan openreceive:notifications
656
711
  ```
657
712
 
658
- It listens for the wallet's NWC-02 `payment_received` notifications and runs
659
- a periodic reconcile pass in the same process — one per deployment, not per
660
- web instance. `php artisan openreceive:reconcile` is the one-shot pass if you
661
- want to drive one yourself, and `php artisan openreceive:doctor` reports every
662
- credential as set/unset, the host and its placeholders, the route mount and
663
- the wallet preflight.
713
+ The worker listens for the wallet's NWC-02 `payment_received` notifications.
714
+ The same process also runs a periodic reconcile pass. Run one per deployment,
715
+ not one per web instance.
716
+
717
+ Two more commands help:
718
+
719
+ - `php artisan openreceive:reconcile` runs one pass, if you want to drive it
720
+ yourself.
721
+ - `php artisan openreceive:doctor` reports every credential as set or unset,
722
+ the host and its placeholders, the route mount, and the wallet preflight.
664
723
 
665
724
  ### Swap secrets
666
725
 
667
- The package recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP` using the
668
- shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors: setting either one
669
- auto-builds the matching provider, so an app that wants swaps only supplies the
670
- connection strings ([Environment variables](https://openreceive.org/guides/environment-variables.md)). The
671
- `openreceive.swap_providers` container binding is the override knob — bind a
672
- list of your own adapters to replace the auto-built set, or `[]` to disable
673
- swaps.
726
+ The package recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP`, using the
727
+ shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors. Setting
728
+ either one auto-builds the matching provider. So an app that wants swaps only
729
+ supplies the connection strings
730
+ ([Environment variables](https://openreceive.org/guides/environment-variables.md)). To override this, use the
731
+ `openreceive.swap_providers` container binding. Bind a list of your own
732
+ adapters to replace the auto-built set, or `[]` to disable swaps.
674
733
 
675
- One `openreceive_payments` row holds at most one provider order in its
734
+ One `openreceive_payments` row holds at most one provider order, in its
676
735
  server-only `swap_data`. The engine never returns `swap_data` from its routes
677
736
  or writes it to a log. Do not select it into your own API, serialize it, or
678
- log it; it may contain a provider credential.
737
+ log it. It may contain a provider credential.
679
738
 
680
739
  **Setting either connection string commits you to refunds.** A swap deposit can
681
- arrive short or late, which leaves it `refund_required` at the provider with
682
- only your UI able to claim it — and the payer claims it on a second visit,
683
- after leaving your page for an address in another wallet. That needs a
684
- per-order URL your app serves, a route that restores the order behind it, and
685
- something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
686
- [Swap refunds](https://openreceive.org/guides/swap-refunds.md) is the whole of it; read it before you set
740
+ arrive short or late. The provider then marks it `refund_required`, and only
741
+ your UI can claim it. The payer claims it on a second visit, after leaving your
742
+ page to get an address in another wallet. That needs three things:
743
+
744
+ - a per-order URL your app serves,
745
+ - a route that restores the order behind it,
746
+ - something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
747
+
748
+ [Swap refunds](https://openreceive.org/guides/swap-refunds.md) covers all of it. Read it before you set
687
749
  `LSC_URI_PRIMARY`.