openreceive-rails 0.4.11 → 0.4.13

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.11.
3
+ These directions describe OpenReceive 0.4.13.
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
 
@@ -312,13 +336,14 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-php.
312
336
 
313
337
  ## PHP quickstart (plain PHP)
314
338
 
315
- Plain PHP, no framework. Requires PHP ≥ 8.2 (64-bit) with `ext-gmp`, `ext-sodium`,
316
- `ext-mbstring`, `ext-json`, `ext-pdo` and one PDO driver (`pdo_pgsql`,
317
- `pdo_sqlite` or `pdo_mysql`). `ext-gmp` is **required**, not optional: the NWC
318
- transport signs every wallet request with it. Laravel has its own quickstart
319
- (`openreceive/laravel`, the thin adapter over this engine); this page is the
320
- one for a host with no framework at all — a front controller, a PDO handle and
321
- 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.
322
347
 
323
348
  ### 1. Install
324
349
 
@@ -326,26 +351,27 @@ three methods.
326
351
  composer require openreceive/openreceive nyholm/psr7 nyholm/psr7-server
327
352
  ```
328
353
 
329
- `openreceive/openreceive` is the whole engine: the receive-only wallet client,
330
- exact money, settlement, the `openreceive_payments` repository over PDO, swaps,
331
- rates and a PSR-15 handler. It depends on the PSR interfaces only, so bring the
332
- PSR-7/PSR-17 implementation your app already has; `nyholm/psr7` +
333
- `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.
334
360
 
335
361
  The **checkout UI is not in the Composer package.** Packagist installs from git
336
- 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
337
363
  `standalone-checkout-<version>.tar.gz` on every
338
- [GitHub release](https://github.com/openreceive/openreceive/releases) — one
339
- self-contained ES module, its stylesheet, a source map and a
340
- `MANIFEST.json`. Unpack it somewhere your web server serves as static files
341
- (step 5). A host with a JS bundler can `npm install @openreceive/elements`
342
- 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.
343
369
 
344
370
  ### 2. Migrate the payment tables
345
371
 
346
- The engine owns two tables in **your** database and renders their DDL per
347
- dialect. Run it through whatever your application uses for schema changes —
348
- 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.
349
375
 
350
376
  ```php
351
377
  use OpenReceive\Storage\PaymentsSchema;
@@ -357,16 +383,18 @@ foreach (PaymentsSchema::statements($dialect) as $sql) {
357
383
  // down(): PaymentsSchema::dropStatements()
358
384
  ```
359
385
 
360
- `PaymentsSchema::migrate(new PdoConnection($pdo))` does the same in one call
361
- for a script that has no migration tool. It creates `openreceive_payments`
362
- (one row per payment attempt) and `openreceive_meta` (the reconcile gate and
363
- the schema version); leave both to the library. Details:
364
- [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).
365
393
 
366
394
  ### 3. Add wallet credentials
367
395
 
368
- Create a server-only `.env` (or export the variables from your process
369
- 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`.
370
398
 
371
399
  ```dotenv
372
400
  NWC_URI=
@@ -375,27 +403,28 @@ LSC_URI_BACKUP=
375
403
  ```
376
404
 
377
405
  1. Get a receive-only NWC code from a compatible wallet
378
- ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
379
- → `NWC_URI`.
380
- 2. Optionally set up a [swap provider](https://openreceive.org/set_up_swap_provider)
381
- → `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP` if you have one).
382
-
383
- Never put these values in browser code. Your application refuses to start if
384
- the NWC code also advertises spend methods such as `pay_invoice`; mint a
385
- receive-only code ([Security](https://openreceive.org/guides/security.md)).
386
-
387
- Nothing in PHP loads a `.env` file on its own; `vlucas/phpdotenv`, your web
388
- server's `SetEnv`/`fastcgi_param`, or the container runtime has to put the
389
- 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
390
419
  ([Environment variables](https://openreceive.org/guides/environment-variables.md)).
391
420
 
392
421
  ### 4. Wire OpenReceive
393
422
 
394
423
  Three methods on one object are the entire bridge between the engine and your
395
- data; the engine never sees an order, a user or a price except through them.
396
- Then `Engine` composes the wallet, the repository over your PDO and that
397
- object into a PSR-15 handler, which your front controller dispatches to under
398
- 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:
399
428
 
400
429
  ```php
401
430
  <?php
@@ -481,57 +510,73 @@ if (str_starts_with($path, '/openreceive')) {
481
510
  ```
482
511
 
483
512
  `Service::fromEnvironment()` builds the wallet client from `NWC_URI` and runs
484
- 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
485
514
  before any route is served. PHP starts every request from nothing, so that
486
- check runs per request that reaches the engine; the settlement gate the
487
- engine relies on lives in `openreceive_meta`, not in memory, which is why a
488
- fleet of PHP-FPM workers shares one wallet-scan budget with no worker of its
489
- own. Later OpenReceive requests also settle pending invoices, so a payer who
490
- 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.
491
522
  → [Engine](https://openreceive.org/guides/api-reference.md#openreceiveserverengine) ·
492
523
  [Host](https://openreceive.org/guides/api-reference.md#openreceivehost) ·
493
524
  [the authorize context](https://openreceive.org/guides/api-reference.md#the-authorize-context-php)
494
525
 
495
- **Cross-site requests.** Plain PHP has no CSRF layer, exactly like Express, and
496
- the engine does not need one: every mounted route refuses a request whose
497
- `Sec-Fetch-Site` header says `cross-site`, so a form or script on another
498
- origin cannot mint invoices with a payer's cookie. `<meta name="csrf-token">`
499
- is therefore optional — set it and the checkout sends the value back as
500
- `X-CSRF-Token` (or the header named by `csrf-header`) for your own layer to
501
- check. What the engine's check does NOT cover: a browser too old to send
502
- `Sec-Fetch-Site` (the header is absent, and absent passes), and anything that
503
- is not a browser at all — a script holding a stolen cookie is a session
504
- problem, not a forgery problem. `authorize` is still the boundary that decides
505
- whether *this caller* may act on *this order* ([Security](https://openreceive.org/guides/security.md)).
506
-
507
- For public web shops, opt into the per-IP invoice cap with
508
- `rateLimiting: true` on `Engine`; leave it off (the default) when many payers
509
- share one IP. Behind a proxy pass `clientIp: fn ($request) => …` so the cap
510
- counts the payer, not the proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
511
-
512
- Your app also needs an ordinary order-creation route that validates the cart,
513
- prices with exact decimal math, and returns the order id the page will pass as
514
- the `reference`. OpenReceive never prices from payer input. The `reference` is
515
- a string you choose, and it is the fulfillment identity: your order id — one
516
- per thing you fulfill, created before checkout, kept across retries, never
517
- reused. `onPaid` commits fulfillment once per reference, a new checkout under a reference
518
- that already settled is refused with 409, and a fresh id per page load lets
519
- one order be paid twice.
520
-
521
- Naming boundary: PHP APIs use camelCase methods and snake_case array keys
522
- (`amount_msats`, `payment_hash`), matching the wire — the mounted HTTP routes
523
- 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.
524
568
 
525
569
  ### 5. Render checkout
526
570
 
527
- Serve the compiled `styles.css` without Tailwind processing: import it from
528
- JavaScript (with a CSS-capable bundler) or use a plain `<link rel="stylesheet">`.
529
- Do not `@import` it into the host Tailwind entry. Its zero-specificity rules
530
- 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.
531
576
 
532
577
  Unpack the release's `standalone-checkout-<version>.tar.gz` into a directory
533
- your web server serves — `public/openreceive/` here — and add two tags plus
534
- the element:
578
+ your web server serves. This page uses `public/openreceive/`. Then add two tags
579
+ and the element:
535
580
 
536
581
  ```html
537
582
  <link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
@@ -543,32 +588,33 @@ the element:
543
588
  ></openreceive-checkout>
544
589
  ```
545
590
 
546
- The module registers `<openreceive-checkout>` as it loads; the element creates
591
+ The module registers `<openreceive-checkout>` as it loads. The element creates
547
592
  the checkout for `reference`, then renders, polls and settles itself. The
548
- stylesheet is scoped to what OpenReceive renders. The checkout follows the payer's theme; on a page that is
549
- always one theme, lock it with `theme="dark"`. React/Vue/Svelte/Angular apps
550
- use the matching wrapper package instead — same attributes
551
- ([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
552
598
  `@openreceive/browser/headless` ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
553
599
 
554
600
  Everything the checkout draws ships inside the JavaScript: the payment-method
555
601
  icons, the wallet logos and the pay tutorials. There is no image file to copy
556
602
  or serve and no asset option to set. Deploy your normal JavaScript and CSS
557
603
  build output, including any generated JavaScript chunks. Bundlers with code
558
- splitting can defer tutorial screenshots until first open; single-file builds
559
- (including the standalone checkout) include them upfront. If your
560
- 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:`
561
607
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
562
608
 
563
- `MANIFEST.json` in the tarball carries the version and a SHA-256 per file, so a
564
- copied tree can be checked against the release it came from; keep the tarball
565
- 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.
566
612
 
567
- A runnable illustration of this boundary — not a template to copy models from —
568
- is Buy a Button
569
- (`examples/buttons/server/php-plain`).
570
- It has products, visitors, and orders, with the three hooks as the only bridge.
571
- 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.
572
618
 
573
619
  ### 6. Verify
574
620
 
@@ -581,28 +627,33 @@ foreach (\OpenReceive\Server\Doctor::report(
581
627
  ) as $line) echo $line, PHP_EOL;
582
628
  ```
583
629
 
584
- `Doctor::report()` prints every credential as set/unset (never a value), the
585
- host class and which of the three methods are still the scaffolded
586
- placeholders (`Hosts\AllowAllAuthorize`, `Hosts\LoggingOnPaid` — the engine
587
- also warns at boot while either is in use), where the handler is mounted, and
588
- the receive-only wallet preflight. `$engine->doctor()` is the same report for
589
- an engine you already built. Put it behind a `bin/doctor` script; the demo's
590
- 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)
591
642
 
592
- 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
593
644
  wallet logos render, and open a wallet's pay tutorial to check its screenshots.
594
- 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
595
646
  panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
596
647
  complete build output. Do not add image routes, copy package source images, or
597
648
  use registry `icon_path` / tutorial `path` keys as browser URLs.
598
649
 
599
650
  ### Reconciliation
600
651
 
601
- Settlement runs on the request path: every payment route first runs one
602
- bounded reconcile pass through the durable `openreceive_meta` gate (minimum 2
603
- seconds between real wallet scans, shared by every PHP process). You do not
604
- need a cron job. Tune or disable it with `Engine`'s `opportunisticReconcile`
605
- (`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 3 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' => …]`).
606
657
 
607
658
  Optionally, run one worker so settlement does not wait for the next page load:
608
659
 
@@ -610,18 +661,20 @@ Optionally, run one worker so settlement does not wait for the next page load:
610
661
  $engine->notificationsWorker()->run(); // blocks: an NWC-02 listener plus a periodic pass
611
662
  ```
612
663
 
613
- as its own long-lived process (`php bin/notifications`). `$engine->reconcile()`
614
- 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()`.
615
666
  → [Engine notificationsWorker](https://openreceive.org/guides/api-reference.md#engine-notificationsworker)
616
667
 
617
668
  ### Swap secrets
618
669
 
619
- Setting `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP`) auto-builds the matching
620
- swap providers; nothing in your code changes. One `openreceive_payments` row
621
- holds at most one provider order in its server-only `swap_data`; the
622
- repository never selects it into public arrays — do not log it or return it
623
- from your own API. **Setting either connection string commits you to
624
- refunds**: a deposit that arrives short or late is claimed on a second visit,
625
- which needs a per-order URL your app serves and the attempt's `payment_hash`
626
- kept. [Swap refunds](https://openreceive.org/guides/swap-refunds.md) is the whole of it; read it before you
627
- 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`.