@openreceive/fastify 0.4.10 → 0.4.12

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 (PHP)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
3
+ These directions describe OpenReceive 0.4.12.
4
4
 
5
5
  Add OpenReceive to a PHP application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the engine is on Packagist
@@ -35,75 +35,99 @@ 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 `OpenReceive\Host`;
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 running `composer require` or editing files.
46
-
47
- 1. Look for `NWC_URI` in this app's server environment — `.env`, the process
48
- env, the web server's `SetEnv`/`fastcgi_param`, the deploy config, whatever
49
- this app already uses. If the app runs in a container the value is in none
50
- of those: ask the running process
51
- (`docker exec <container> printenv NWC_URI`), because finding the NAME in a
52
- compose file proves nothing about the value. Never print or echo the value
53
- itself; only report whether it is set. Check for `LSC_URI_PRIMARY` in the
54
- same pass.
55
-
56
- If OpenReceive is already installed here, `OpenReceive\Server\Doctor::report()`
57
- answers this whole step in one command — every credential as set/unset, the
58
- host class and which hooks are still placeholders, the mount, and the wallet
59
- preflight. It never prints a value. A plain host runs it from a `bin/doctor`
60
- script (the quickstart's step 6 is the whole script).
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. Check the PHP runtime: `php -m` must list `gmp`, `sodium`, `mbstring`,
97
- `json`, `pdo` and one PDO driver. `ext-gmp` is REQUIRED — the NWC
98
- transport signs every request with it — and `php:*-cli`/`-fpm` images do
99
- not ship it (`docker-php-ext-install gmp`). PHP must be ≥ 8.2 and 64-bit.
100
- 7. If OpenReceive is ALREADY installed here, check the installed
101
- `openreceive/openreceive` version (`composer show openreceive/openreceive`)
102
- and the unpacked checkout's `MANIFEST.json` against the release named at the
103
- top of this file. The two must match: the browser build and the engine are
104
- one release. Upgrade first — and if this app runs in containers, rebuild the
105
- images: `vendor/` is baked into the image, so an in-place `composer update`
106
- 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 run `composer require`,
52
+ edit the app, write `.env.example`, or search anywhere else before asking
53
+ it. Do not read deploy config (compose files, `SetEnv`/`fastcgi_param` blocks, platform secrets), 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.** Nothing in PHP
102
+ reads `.env` on its own. Add `vlucas/phpdotenv` and load the file in the
103
+ front controller before `Service::fromEnvironment()`. When the app is started
104
+ with Docker Compose, give the service `env_file: .env`. Restart PHP-FPM or
105
+ the server after writing the file.
106
+
107
+ Do not invent placeholder URIs. Start the quickstart only once `NWC_URI` is
108
+ saved and `LSC_URI_PRIMARY` is saved or explicitly declined. The first boot
109
+ runs the receive-only preflight: if it reports spend methods such as
110
+ `pay_invoice`, remove `NWC_URI` from `.env` and ask for a receive-only code
111
+ again. Never set the spend-capable override to get past it.
112
+
113
+ Then check the PHP runtime: `php -m` must list `gmp`, `sodium`, `mbstring`,
114
+ `json`, `pdo` and one PDO driver. `ext-gmp` is REQUIRED — the NWC transport
115
+ signs every request with it — and `php:*-cli`/`-fpm` images do not ship it
116
+ (`docker-php-ext-install gmp`). PHP must be ≥ 8.2 and 64-bit.
117
+
118
+ ### Upgrading an existing install
119
+
120
+ This is not the opening move of a new integration. When OpenReceive is ALREADY
121
+ installed here, `OpenReceive\Server\Doctor::report()` (a plain host runs it
122
+ from the quickstart's `bin/doctor` script) reports every credential as
123
+ set/unset (never the value), the host class and any placeholder hooks, the
124
+ mount, and the wallet preflight. Check the installed `openreceive/openreceive`
125
+ (`composer show openreceive/openreceive`) and the unpacked checkout's
126
+ `MANIFEST.json` against the release named at the top of this file. The two
127
+ must match: the browser build and the engine are one release. Upgrade first —
128
+ and if this app runs in containers, rebuild the images: `vendor/` is baked into
129
+ the image, so an in-place `composer update` is undone by the next
130
+ `compose up`.
107
131
 
108
132
  Only then start the quickstart.
109
133
 
@@ -125,7 +149,7 @@ itself, and they hold for every integration.
125
149
  is a placeholder that allows everything (the engine warns at boot while a
126
150
  host uses it) — replace it with this app's real ownership check, same as
127
151
  `onPaid`'s `Hosts\LoggingOnPaid`.
128
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
152
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
129
153
  per thing you fulfill, created before checkout, kept across retries, never
130
154
  reused. A fresh id per page load lets one order be paid twice.
131
155
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -301,6 +325,8 @@ enough; drop the `.md` for the same page a person would read.
301
325
  Questions, or a problem with the library itself:
302
326
  https://openreceive.org/contact
303
327
 
328
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
329
+
304
330
  ---
305
331
 
306
332
  ## The quickstart, in full
@@ -310,13 +336,14 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-php.
310
336
 
311
337
  ## PHP quickstart (plain PHP)
312
338
 
313
- Plain PHP, no framework. Requires PHP ≥ 8.2 (64-bit) with `ext-gmp`, `ext-sodium`,
314
- `ext-mbstring`, `ext-json`, `ext-pdo` and one PDO driver (`pdo_pgsql`,
315
- `pdo_sqlite` or `pdo_mysql`). `ext-gmp` is **required**, not optional: the NWC
316
- transport signs every wallet request with it. Laravel has its own quickstart
317
- (`openreceive/laravel`, the thin adapter over this engine); this page is the
318
- one for a host with no framework at all — a front controller, a PDO handle and
319
- three methods.
339
+ This page is for plain PHP, with no framework: a front controller, a PDO handle
340
+ and three methods. Laravel has its own quickstart (`openreceive/laravel`, the
341
+ thin adapter over this engine).
342
+
343
+ Requires PHP ≥ 8.2 (64-bit) with `ext-gmp`, `ext-sodium`, `ext-mbstring`,
344
+ `ext-json`, `ext-pdo` and one PDO driver (`pdo_pgsql`, `pdo_sqlite` or
345
+ `pdo_mysql`). `ext-gmp` is **required**, not optional. The NWC transport signs
346
+ every wallet request with it.
320
347
 
321
348
  ### 1. Install
322
349
 
@@ -324,26 +351,27 @@ three methods.
324
351
  composer require openreceive/openreceive nyholm/psr7 nyholm/psr7-server
325
352
  ```
326
353
 
327
- `openreceive/openreceive` is the whole engine: the receive-only wallet client,
328
- exact money, settlement, the `openreceive_payments` repository over PDO, swaps,
329
- rates and a PSR-15 handler. It depends on the PSR interfaces only, so bring the
330
- PSR-7/PSR-17 implementation your app already has; `nyholm/psr7` +
331
- `nyholm/psr7-server` is the smallest pair and the one this page uses.
354
+ `openreceive/openreceive` is the whole engine. It includes the receive-only
355
+ wallet client, exact money, settlement, the `openreceive_payments` repository
356
+ over PDO, swaps, rates and a PSR-15 handler. It depends only on the PSR
357
+ interfaces, so bring the PSR-7/PSR-17 implementation your app already has.
358
+ `nyholm/psr7` + `nyholm/psr7-server` is the smallest pair, and this page uses
359
+ it.
332
360
 
333
361
  The **checkout UI is not in the Composer package.** Packagist installs from git
334
- and cannot run a JS build, so the browser side ships separately as
362
+ and cannot run a JS build. So the browser side ships separately, as
335
363
  `standalone-checkout-<version>.tar.gz` on every
336
- [GitHub release](https://github.com/openreceive/openreceive/releases) — one
337
- self-contained ES module, its stylesheet, a source map and a
338
- `MANIFEST.json`. Unpack it somewhere your web server serves as static files
339
- (step 5). A host with a JS bundler can `npm install @openreceive/elements`
340
- instead; the tarball is the same build.
364
+ [GitHub release](https://github.com/openreceive/openreceive/releases). It holds
365
+ one self-contained ES module, its stylesheet, a source map and a
366
+ `MANIFEST.json`. Unpack it somewhere your web server serves static files
367
+ (step 5). If your app has a JS bundler, you can `npm install @openreceive/elements`
368
+ instead. The tarball is the same build.
341
369
 
342
370
  ### 2. Migrate the payment tables
343
371
 
344
- The engine owns two tables in **your** database and renders their DDL per
345
- dialect. Run it through whatever your application uses for schema changes —
346
- Phinx, Doctrine Migrations, a plain SQL file, a `bin/migrate` script:
372
+ The engine owns two tables in **your** database and renders their DDL for each
373
+ SQL dialect. Run that DDL through whatever your app uses for schema changes:
374
+ Phinx, Doctrine Migrations, a plain SQL file, or a `bin/migrate` script.
347
375
 
348
376
  ```php
349
377
  use OpenReceive\Storage\PaymentsSchema;
@@ -355,16 +383,18 @@ foreach (PaymentsSchema::statements($dialect) as $sql) {
355
383
  // down(): PaymentsSchema::dropStatements()
356
384
  ```
357
385
 
358
- `PaymentsSchema::migrate(new PdoConnection($pdo))` does the same in one call
359
- for a script that has no migration tool. It creates `openreceive_payments`
360
- (one row per payment attempt) and `openreceive_meta` (the reconcile gate and
361
- the schema version); leave both to the library. Details:
362
- [Payment storage](https://openreceive.org/guides/storage.md).
386
+ If your script has no migration tool, `PaymentsSchema::migrate(new PdoConnection($pdo))`
387
+ does the same in one call. It creates two tables. Leave both to the library:
388
+
389
+ - `openreceive_payments`: one row per payment attempt.
390
+ - `openreceive_meta`: the reconcile gate and the schema version.
391
+
392
+ Details: [Payment storage](https://openreceive.org/guides/storage.md).
363
393
 
364
394
  ### 3. Add wallet credentials
365
395
 
366
- Create a server-only `.env` (or export the variables from your process
367
- manager — the engine reads `getenv()` and `$_ENV`):
396
+ Create a server-only `.env`, or export the variables from your process manager.
397
+ The engine reads `getenv()` and `$_ENV`.
368
398
 
369
399
  ```dotenv
370
400
  NWC_URI=
@@ -373,27 +403,28 @@ LSC_URI_BACKUP=
373
403
  ```
374
404
 
375
405
  1. Get a receive-only NWC code from a compatible wallet
376
- ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
377
- → `NWC_URI`.
378
- 2. Optionally set up a [swap provider](https://openreceive.org/set_up_swap_provider)
379
- → `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP` if you have one).
380
-
381
- Never put these values in browser code. Your application refuses to start if
382
- the NWC code also advertises spend methods such as `pay_invoice`; mint a
383
- receive-only code ([Security](https://openreceive.org/guides/security.md)).
384
-
385
- Nothing in PHP loads a `.env` file on its own; `vlucas/phpdotenv`, your web
386
- server's `SetEnv`/`fastcgi_param`, or the container runtime has to put the
387
- values in the process environment first
406
+ ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
407
+ Put it in `NWC_URI`.
408
+ 2. Optional: set up a [swap provider](https://openreceive.org/set_up_swap_provider).
409
+ Put its connection string in `LSC_URI_PRIMARY`, and a second one in
410
+ `LSC_URI_BACKUP` if you have one.
411
+
412
+ Never put these values in browser code. Your app refuses to start if the NWC
413
+ code also advertises spend methods such as `pay_invoice`. Create a
414
+ receive-only code instead ([Security](https://openreceive.org/guides/security.md)).
415
+
416
+ Nothing in PHP loads a `.env` file on its own. Something has to put the values
417
+ in the process environment first: `vlucas/phpdotenv`, your web server's
418
+ `SetEnv`/`fastcgi_param`, or the container runtime
388
419
  ([Environment variables](https://openreceive.org/guides/environment-variables.md)).
389
420
 
390
421
  ### 4. Wire OpenReceive
391
422
 
392
423
  Three methods on one object are the entire bridge between the engine and your
393
- data; the engine never sees an order, a user or a price except through them.
394
- Then `Engine` composes the wallet, the repository over your PDO and that
395
- object into a PSR-15 handler, which your front controller dispatches to under
396
- one path prefix:
424
+ data. The engine never sees an order, a user or a price except through them.
425
+ `Engine` combines the wallet, the repository over your PDO, and that object
426
+ into a PSR-15 handler. Your front controller sends requests under one path
427
+ prefix to that handler:
397
428
 
398
429
  ```php
399
430
  <?php
@@ -479,57 +510,73 @@ if (str_starts_with($path, '/openreceive')) {
479
510
  ```
480
511
 
481
512
  `Service::fromEnvironment()` builds the wallet client from `NWC_URI` and runs
482
- the receive-only preflight — a missing, invalid or spend-capable code throws
513
+ the receive-only preflight. A missing, invalid or spend-capable code throws
483
514
  before any route is served. PHP starts every request from nothing, so that
484
- check runs per request that reaches the engine; the settlement gate the
485
- engine relies on lives in `openreceive_meta`, not in memory, which is why a
486
- fleet of PHP-FPM workers shares one wallet-scan budget with no worker of its
487
- own. Later OpenReceive requests also settle pending invoices, so a payer who
488
- closes the tab is still covered. `authorize` runs on every request.
515
+ check runs on every request that reaches the engine.
516
+
517
+ The settlement gate the engine relies on lives in `openreceive_meta`, not in
518
+ memory. That is why a fleet of PHP-FPM workers shares one wallet-scan budget,
519
+ with no worker process of its own. Later OpenReceive requests also settle
520
+ pending invoices, so a payer who closes the tab is still covered. `authorize`
521
+ runs on every request.
489
522
  → [Engine](https://openreceive.org/guides/api-reference.md#openreceiveserverengine) ·
490
523
  [Host](https://openreceive.org/guides/api-reference.md#openreceivehost) ·
491
524
  [the authorize context](https://openreceive.org/guides/api-reference.md#the-authorize-context-php)
492
525
 
493
- **Cross-site requests.** Plain PHP has no CSRF layer, exactly like Express, and
494
- the engine does not need one: every mounted route refuses a request whose
495
- `Sec-Fetch-Site` header says `cross-site`, so a form or script on another
496
- origin cannot mint invoices with a payer's cookie. `<meta name="csrf-token">`
497
- is therefore optional — set it and the checkout sends the value back as
498
- `X-CSRF-Token` (or the header named by `csrf-header`) for your own layer to
499
- check. What the engine's check does NOT cover: a browser too old to send
500
- `Sec-Fetch-Site` (the header is absent, and absent passes), and anything that
501
- is not a browser at all — a script holding a stolen cookie is a session
502
- problem, not a forgery problem. `authorize` is still the boundary that decides
503
- whether *this caller* may act on *this order* ([Security](https://openreceive.org/guides/security.md)).
504
-
505
- For public web shops, opt into the per-IP invoice cap with
506
- `rateLimiting: true` on `Engine`; leave it off (the default) when many payers
507
- share one IP. Behind a proxy pass `clientIp: fn ($request) => …` so the cap
508
- counts the payer, not the proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
509
-
510
- Your app also needs an ordinary order-creation route that validates the cart,
511
- prices with exact decimal math, and returns the order id the page will pass as
512
- the `reference`. OpenReceive never prices from payer input. The `reference` is
513
- a string you choose, and it is the fulfillment identity: your order id — one
514
- per thing you fulfill, created before checkout, kept across retries, never
515
- reused. `onPaid` runs once per reference, a new checkout under a reference
516
- that already settled is refused with 409, and a fresh id per page load lets
517
- one order be paid twice.
518
-
519
- Naming boundary: PHP APIs use camelCase methods and snake_case array keys
520
- (`amount_msats`, `payment_hash`), matching the wire — the mounted HTTP routes
521
- and the browser snapshots are snake_case throughout.
526
+ **Cross-site requests.** Plain PHP has no CSRF layer, just like Express, and the
527
+ engine does not need one. Every mounted route refuses a request whose
528
+ `Sec-Fetch-Site` header says `cross-site`. So a form or script on another
529
+ origin cannot create invoices using a payer's cookie. That makes
530
+ `<meta name="csrf-token">` optional. If you set it, the checkout sends the value
531
+ back as `X-CSRF-Token` (or the header named by `csrf-header`) for your own layer
532
+ to check.
533
+
534
+ The engine's check does NOT cover two cases:
535
+
536
+ - A browser too old to send `Sec-Fetch-Site`. The header is absent, and an
537
+ absent header passes.
538
+ - Anything that is not a browser at all. A script holding a stolen cookie is a
539
+ session problem, not a forgery problem.
540
+
541
+ `authorize` is still the boundary that decides whether *this caller* may act on
542
+ *this order* ([Security](https://openreceive.org/guides/security.md)).
543
+
544
+ For public web shops, turn on the per-IP invoice cap with `rateLimiting: true`
545
+ on `Engine`. Leave it off (the default) when many payers share one IP. Behind a
546
+ proxy, pass `clientIp: fn ($request) => …` so the cap counts the payer, not the
547
+ proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
548
+
549
+ Your app also needs an ordinary order-creation route. It validates the cart,
550
+ prices with exact decimal math, and returns the order id. The page then passes
551
+ that id as the `reference`. OpenReceive never prices from payer input.
552
+
553
+ The `reference` is a string you choose, and it is the fulfillment identity. Use
554
+ your order id:
555
+
556
+ - one per thing you fulfill,
557
+ - created before checkout,
558
+ - kept across retries,
559
+ - never reused.
560
+
561
+ `onPaid` commits fulfillment once per reference, and a new checkout under a
562
+ reference that already settled is refused with 409. A fresh id per page load
563
+ would let one order be paid twice.
564
+
565
+ Naming: PHP APIs use camelCase methods and snake_case array keys
566
+ (`amount_msats`, `payment_hash`). The keys match the wire format. The mounted
567
+ HTTP routes and the browser snapshots are snake_case throughout.
522
568
 
523
569
  ### 5. Render checkout
524
570
 
525
- Serve the compiled `styles.css` without Tailwind processing: import it from
526
- JavaScript (with a CSS-capable bundler) or use a plain `<link rel="stylesheet">`.
527
- Do not `@import` it into the host Tailwind entry. Its zero-specificity rules
528
- allow host styles to override checkout styles; scoping does not prevent that.
571
+ Serve the compiled `styles.css` without Tailwind processing. Either import it
572
+ from JavaScript (with a CSS-capable bundler) or use a plain
573
+ `<link rel="stylesheet">`. Do not `@import` it into your Tailwind entry. Its
574
+ rules have zero specificity, so your own styles can override checkout styles.
575
+ Scoping does not prevent that.
529
576
 
530
577
  Unpack the release's `standalone-checkout-<version>.tar.gz` into a directory
531
- your web server serves — `public/openreceive/` here — and add two tags plus
532
- the element:
578
+ your web server serves. This page uses `public/openreceive/`. Then add two tags
579
+ and the element:
533
580
 
534
581
  ```html
535
582
  <link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
@@ -541,32 +588,33 @@ the element:
541
588
  ></openreceive-checkout>
542
589
  ```
543
590
 
544
- The module registers `<openreceive-checkout>` as it loads; the element creates
591
+ The module registers `<openreceive-checkout>` as it loads. The element creates
545
592
  the checkout for `reference`, then renders, polls and settles itself. The
546
- stylesheet is scoped to what OpenReceive renders. The checkout follows the payer's theme; on a page that is
547
- always one theme, lock it with `theme="dark"`. React/Vue/Svelte/Angular apps
548
- use the matching wrapper package instead — same attributes
549
- ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)); a custom UI builds on
593
+ stylesheet is scoped to what OpenReceive renders. The checkout follows the
594
+ payer's theme. On a page that always uses one theme, lock it with
595
+ `theme="dark"`. React, Vue, Svelte, and Angular apps use the matching wrapper
596
+ package instead, with the same attributes
597
+ ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). A custom UI builds on
550
598
  `@openreceive/browser/headless` ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
551
599
 
552
600
  Everything the checkout draws ships inside the JavaScript: the payment-method
553
601
  icons, the wallet logos and the pay tutorials. There is no image file to copy
554
602
  or serve and no asset option to set. Deploy your normal JavaScript and CSS
555
603
  build output, including any generated JavaScript chunks. Bundlers with code
556
- splitting can defer tutorial screenshots until first open; single-file builds
557
- (including the standalone checkout) include them upfront. If your
558
- Content-Security-Policy has a strict `img-src`, allow `data:`
604
+ splitting can load tutorial screenshots only when a tutorial is first opened.
605
+ Single-file builds, including the standalone checkout, include them upfront. If
606
+ your Content-Security-Policy has a strict `img-src`, allow `data:`
559
607
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
560
608
 
561
- `MANIFEST.json` in the tarball carries the version and a SHA-256 per file, so a
562
- copied tree can be checked against the release it came from; keep the tarball
563
- version in step with the Composer package.
609
+ `MANIFEST.json` in the tarball carries the version and a SHA-256 per file. You
610
+ can use it to check a copied tree against the release it came from. Keep the
611
+ tarball version in step with the Composer package.
564
612
 
565
- A runnable illustration of this boundary — not a template to copy models from —
566
- is Buy a Button
567
- (`examples/buttons/server/php-plain`).
568
- It has products, visitors, and orders, with the three hooks as the only bridge.
569
- Map that shape onto the models in THIS app.
613
+ Buy a Button
614
+ (`examples/buttons/server/php-plain`)
615
+ is a runnable illustration of this boundary. It is not a template to copy
616
+ models from. It has products, visitors, and orders, and the three hooks are the
617
+ only bridge. Map that shape onto the models in THIS app.
570
618
 
571
619
  ### 6. Verify
572
620
 
@@ -579,28 +627,33 @@ foreach (\OpenReceive\Server\Doctor::report(
579
627
  ) as $line) echo $line, PHP_EOL;
580
628
  ```
581
629
 
582
- `Doctor::report()` prints every credential as set/unset (never a value), the
583
- host class and which of the three methods are still the scaffolded
584
- placeholders (`Hosts\AllowAllAuthorize`, `Hosts\LoggingOnPaid` — the engine
585
- also warns at boot while either is in use), where the handler is mounted, and
586
- the receive-only wallet preflight. `$engine->doctor()` is the same report for
587
- an engine you already built. Put it behind a `bin/doctor` script; the demo's
588
- is twelve lines. → [Doctor](https://openreceive.org/guides/api-reference.md#openreceiveserverdoctor)
630
+ `Doctor::report()` prints:
631
+
632
+ - every credential as set or unset, never its value;
633
+ - the host class, and which of the three methods are still the scaffolded
634
+ placeholders (`Hosts\AllowAllAuthorize`, `Hosts\LoggingOnPaid`). The engine
635
+ also warns at boot while either is in use;
636
+ - where the handler is mounted;
637
+ - the receive-only wallet preflight.
638
+
639
+ `$engine->doctor()` is the same report for an engine you already built. Put it
640
+ behind a `bin/doctor` script. The demo's is twelve lines.
641
+ → [Doctor](https://openreceive.org/guides/api-reference.md#openreceiveserverdoctor)
589
642
 
590
- Then open the checkout in a browser, confirm the payment-method icons and
643
+ Then open the checkout in a browser. Confirm the payment-method icons and
591
644
  wallet logos render, and open a wallet's pay tutorial to check its screenshots.
592
- If an image is missing, inspect the console for CSP violations and the Network
645
+ If an image is missing, check the console for CSP violations and the Network
593
646
  panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
594
647
  complete build output. Do not add image routes, copy package source images, or
595
648
  use registry `icon_path` / tutorial `path` keys as browser URLs.
596
649
 
597
650
  ### Reconciliation
598
651
 
599
- Settlement runs on the request path: every payment route first runs one
600
- bounded reconcile pass through the durable `openreceive_meta` gate (minimum 2
601
- seconds between real wallet scans, shared by every PHP process). You do not
602
- need a cron job. Tune or disable it with `Engine`'s `opportunisticReconcile`
603
- (`false`, or `['min_interval_seconds' => …]`).
652
+ Settlement runs on the request path. Every payment route first runs one bounded
653
+ reconcile pass through the durable `openreceive_meta` gate. The gate allows at
654
+ most one real wallet scan every 2 seconds, shared by every PHP process. You do
655
+ not need a cron job. Tune or disable it with `Engine`'s
656
+ `opportunisticReconcile` (`false`, or `['min_interval_seconds' => …]`).
604
657
 
605
658
  Optionally, run one worker so settlement does not wait for the next page load:
606
659
 
@@ -608,18 +661,20 @@ Optionally, run one worker so settlement does not wait for the next page load:
608
661
  $engine->notificationsWorker()->run(); // blocks: an NWC-02 listener plus a periodic pass
609
662
  ```
610
663
 
611
- as its own long-lived process (`php bin/notifications`). `$engine->reconcile()`
612
- is the one-shot pass if you want to drive it yourself.
664
+ Run it as its own long-lived process (`php bin/notifications`). To run a pass
665
+ yourself, use the one-shot `$engine->reconcile()`.
613
666
  → [Engine notificationsWorker](https://openreceive.org/guides/api-reference.md#engine-notificationsworker)
614
667
 
615
668
  ### Swap secrets
616
669
 
617
- Setting `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP`) auto-builds the matching
618
- swap providers; nothing in your code changes. One `openreceive_payments` row
619
- holds at most one provider order in its server-only `swap_data`; the
620
- repository never selects it into public arrays — do not log it or return it
621
- from your own API. **Setting either connection string commits you to
622
- refunds**: a deposit that arrives short or late is claimed on a second visit,
623
- which needs a per-order URL your app serves and the attempt's `payment_hash`
624
- kept. [Swap refunds](https://openreceive.org/guides/swap-refunds.md) is the whole of it; read it before you
625
- set `LSC_URI_PRIMARY`.
670
+ Setting `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP`) auto-builds the matching swap
671
+ providers. Nothing in your code changes. One `openreceive_payments` row holds
672
+ at most one provider order, in its server-only `swap_data`. The repository
673
+ never selects it into public arrays. Do not log it or return it from your own
674
+ API.
675
+
676
+ **Setting either connection string commits you to refunds.** A deposit that
677
+ arrives short or late is claimed on a second visit. That needs a per-order URL
678
+ your app serves, and the attempt's `payment_hash` kept.
679
+ [Swap refunds](https://openreceive.org/guides/swap-refunds.md) covers all of it. Read it before you set
680
+ `LSC_URI_PRIMARY`.