openreceive-rails 0.4.13 → 0.4.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c17784f21721177e139d27349d7fa0367f2e6f90cce005e5dd61906ac5fae96a
4
- data.tar.gz: f285c9fb57805628956740699f078115672f1485916784fbc2e4b6d8738b1578
3
+ metadata.gz: a60b35a3d97d75b923597623ae81c053fd2a7632881f0b6e912da6cf17d56823
4
+ data.tar.gz: e0eb5f150b5a591cdc8fe20007174da8154f3466a56f6b7b2a5b2abacca4b291
5
5
  SHA512:
6
- metadata.gz: 16634ea35e356230c34b4da9ecf58bc8c089835d1b919692e41ddda40ef127fbddc23e5aa3090ba62835f90ba23b393d562a9006df015cbb5a84c2c16ec0ee35
7
- data.tar.gz: 5d70f67f044bfa1003fb963da3e3e8ffe620f27517662d8fd1199ae0972906c7fcc1172196b8b8704d9c4354485700e2732dcf7d99ea818772cb19999a045296
6
+ metadata.gz: fef5b00ebad98f38e75bfc397c71c19e1743f621a92ff78462cb553efdb35a2d0e6112170808883fd44171e77d6a5b92e98a9e34ed5d05b2360970238f82df0f
7
+ data.tar.gz: 97b42ba9c0037f31321d2ce1fb875d4dad86284631d2b3981bb7f19219a82023876d94253735da0b1ce63cceff20d0492b524e3a33302f02db45bf6cc6a9e058
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.15 - 2026-10-06
4
+
5
+ - Install agent skills with `bin/rails openreceive:skills` (optional
6
+ `--dir .claude/skills`). The core `openreceive` gem owns the one offline
7
+ bundle; Rails/server gems no longer duplicate it. Other Ruby hosts can use
8
+ `npx skills add OpenReceive/openreceive`.
9
+
10
+ Requires `nwc-ruby ~> 0.3`. On 0.2.x a silent relay, or an offline wallet
11
+ behind a live relay, could block a checkout's `make_invoice` and the boot
12
+ preflight indefinitely, holding a web thread. 0.3 bounds every call by
13
+ `request_timeout`, retries only failures from before the request is written,
14
+ and tries each relay in the connection string. Relay failures now answer 503
15
+ `WALLET_UNAVAILABLE` (retryable) instead of 502 `OTHER`.
16
+
17
+ ## 0.4.14 - 2026-10-04
18
+
19
+ Fixes two 0.4.13 reconciliation regressions. A host-clock attempt could be
20
+ skipped indefinitely: with one scan-window slot taken, selection moved its
21
+ cursor past the whole batch and dropped the batch's host-clock attempts. The
22
+ cursor now moves only past attempts it has queued. And a wallet that answers
23
+ only one history page per slice re-read the same overlap page forever; an
24
+ overlap answered alone is now spent, and the next slice reads the unseen page
25
+ first. Both are pinned by new `reconcile-progress.json` vectors. The bundled
26
+ integration skill also documents the plain-HTML checkout's way back to a swap
27
+ refund (`resume-payment-hash`, `resumable`, the `openreceive-state` event).
28
+
3
29
  ## 0.4.13 - 2026-10-01
4
30
 
5
31
  Reconciliation no longer livelocks on a slow wallet. A wallet-history page
@@ -150,7 +176,8 @@ warning links the rate-limiting guide.
150
176
 
151
177
  ### The gem carries the agent skills
152
178
 
153
- `skills/` ships in the gem — the integrate and debug playbooks for coding
179
+ At this release, `skills/` shipped in this gem (now supplied by the core
180
+ `openreceive` dependency; see Unreleased) — the integrate and debug playbooks for coding
154
181
  agents, kept byte-identical to the repository tree by
155
182
  `npm run generate:skills`.
156
183
 
data/README.md CHANGED
@@ -131,3 +131,13 @@ Storage-free `openreceive-server` handlers can call `on_paid` on every settled
131
131
  poll, so advanced hosts own the conditional write/outbox. A raw create hook
132
132
  refusal returns 409 with instructions withheld; repository infrastructure failures
133
133
  remain retryable 503.
134
+
135
+ ## Agent skills
136
+
137
+ In Rails, run `bin/rails openreceive:skills` from your application. The offline
138
+ skills ship once in the core `openreceive` gem. Non-Rails projects can use
139
+ `npx skills add OpenReceive/openreceive`.
140
+ See [agent setup](https://openreceive.org/agents). The bundled installers write
141
+ to `.agents/skills/`; use `--dir .claude/skills` for Claude Code. They replace
142
+ only `integrate-openreceive` and `debug-openreceive-payment`, preserving
143
+ unrelated skills.
@@ -5,6 +5,6 @@ module OpenReceive
5
5
  # top-level `::Rails` framework constant — engine code always references the framework as
6
6
  # `::Rails` to avoid shadowing.
7
7
  module Rails
8
- VERSION = "0.4.13"
8
+ VERSION = "0.4.15"
9
9
  end
10
10
  end
@@ -82,15 +82,29 @@ module OpenReceive
82
82
  candidates = OpenReceivePayment.reconcilable_attempts
83
83
  end
84
84
  unless candidates.empty?
85
- last = candidates.last
86
- scheduler["cursor"] = candidates.length < Server::RECONCILE_BATCH_SIZE ? nil : last.slice("created_at", "payment_hash")
87
85
  queued = windows.flat_map { |w| w.fetch("attempts").map { |a| a.fetch("payment_hash") } }
88
- cohort = candidates.reject { |a| queued.include?(a.fetch("payment_hash")) }
89
86
  # A host-clock attempt's window spans the whole wallet history. It
90
87
  # gets its own window, so it never drags wallet-timed attempts into
91
- # that walk; one the cap leaves out returns on cursor wrap.
92
- cohort.partition { |a| a["created_at_source"] == "wallet" }.each do |group|
93
- windows << ReconcileScan.new_window(group, observed_at, overlap_seconds) unless group.empty? || windows.length >= 2
88
+ # that walk. Candidates are taken in keyset order, and the cursor
89
+ # moves only past those already queued or admitted: one whose clock
90
+ # source has no free slot stops the selection and is read again next
91
+ # time, never skipped.
92
+ cohorts = {}
93
+ taken = 0
94
+ candidates.each do |attempt|
95
+ unless queued.include?(attempt.fetch("payment_hash"))
96
+ wallet = attempt["created_at_source"] == "wallet"
97
+ break if !cohorts.key?(wallet) && windows.length + cohorts.length >= 2
98
+
99
+ (cohorts[wallet] ||= []) << attempt
100
+ end
101
+ taken += 1
102
+ end
103
+ # A short batch taken whole already reached the ledger's tail: wrap now.
104
+ tail = taken == candidates.length && candidates.length < Server::RECONCILE_BATCH_SIZE
105
+ scheduler["cursor"] = tail ? nil : candidates[taken - 1].slice("created_at", "payment_hash")
106
+ [true, false].each do |wallet|
107
+ windows << ReconcileScan.new_window(cohorts[wallet], observed_at, overlap_seconds) if cohorts.key?(wallet)
94
108
  end
95
109
  end
96
110
  end
@@ -27,6 +27,15 @@ module OpenReceive
27
27
  anchor = resumed ? window["anchor_offset"] : nil
28
28
  replaying = !anchor.nil?
29
29
  previous = window["fingerprint"]
30
+ # The last answered page was the overlap re-read. A slice that ends here
31
+ # spends the overlap, so the next slice reads the unseen page first: a
32
+ # wallet answering one page per slice still advances. The fingerprint
33
+ # stays, to catch a wallet that ignores offset.
34
+ overlap_only = false
35
+ continued = lambda do
36
+ window["anchor_offset"] = nil if overlap_only
37
+ [results.values, false, false]
38
+ end
30
39
  max_pages.times do |page_number|
31
40
  break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
32
41
 
@@ -47,9 +56,11 @@ module OpenReceive
47
56
  # that never answers stays visible.
48
57
  raise if page_number.zero? || Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
49
58
 
50
- return [results.values, false, false]
59
+ return continued.call
51
60
  end
52
- return [results.values, false, false] if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
61
+ return continued.call if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
62
+
63
+ overlap_only = false
53
64
 
54
65
  rows = page.fetch("transactions")
55
66
  physical = rows.length + page.fetch("skipped_rows", 0)
@@ -73,6 +84,7 @@ module OpenReceive
73
84
  unless physical.zero?
74
85
  window["anchor_offset"] = offset
75
86
  window["fingerprint"] = fingerprint
87
+ overlap_only = true
76
88
  next
77
89
  end
78
90
  end
@@ -100,7 +112,7 @@ module OpenReceive
100
112
  previous = fingerprint
101
113
  return [results.values, true, false] if expected.all? { |hash| %w[settled expired failed].include?(window.fetch("observations").dig(hash, "status")) }
102
114
  end
103
- [results.values, false, false]
115
+ continued.call
104
116
  end
105
117
  end
106
118
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/command"
4
+ require "openreceive/skills"
5
+
6
+ module Rails
7
+ module Command
8
+ # A Rails command accepts --dir before the Rake fallback parses arguments.
9
+ # It does not boot the application or require wallet credentials.
10
+ class OpenreceiveCommand < Base
11
+ desc "skills", "Install the bundled OpenReceive agent skills into this project"
12
+ method_option :dir, type: :string, default: ".agents/skills"
13
+ def skills
14
+ OpenReceive::Skills.install(directory: options[:dir])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :openreceive do
4
+ desc "Install agent skills (optional directory: openreceive:skills[path])"
5
+ task :skills, [:dir] do |_task, args|
6
+ require "openreceive/skills"
7
+ OpenReceive::Skills.install(directory: args[:dir] || ".agents/skills")
8
+ end
9
+
4
10
  # Step 0 of the agent directions, as one command.
5
11
  #
6
12
  # "Look for NWC_URI in this app's server environment" is a SEARCH, and it has
@@ -18,6 +24,7 @@ namespace :openreceive do
18
24
  set = ->(name) { ENV[name].to_s.strip.empty? ? "unset" : "set" }
19
25
  lines = [
20
26
  "openreceive:doctor",
27
+ "Agent skills: run `bin/rails openreceive:skills`",
21
28
  " NWC_URI: #{set.call('NWC_URI')}",
22
29
  " LSC_URI_PRIMARY: #{set.call('LSC_URI_PRIMARY')}",
23
30
  " LSC_URI_BACKUP: #{set.call('LSC_URI_BACKUP')}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openreceive-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.13
4
+ version: 0.4.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenReceive
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.4.13
18
+ version: 0.4.15
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.4.13
25
+ version: 0.4.15
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: openreceive-server
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.4.13
32
+ version: 0.4.15
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.4.13
39
+ version: 0.4.15
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rails
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -57,20 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.2'
61
- - - ">="
62
- - !ruby/object:Gem::Version
63
- version: 0.2.4
60
+ version: '0.3'
64
61
  type: :runtime
65
62
  prerelease: false
66
63
  version_requirements: !ruby/object:Gem::Requirement
67
64
  requirements:
68
65
  - - "~>"
69
66
  - !ruby/object:Gem::Version
70
- version: '0.2'
71
- - - ">="
72
- - !ruby/object:Gem::Version
73
- version: 0.2.4
67
+ version: '0.3'
74
68
  - !ruby/object:Gem::Dependency
75
69
  name: sqlite3
76
70
  requirement: !ruby/object:Gem::Requirement
@@ -130,19 +124,8 @@ files:
130
124
  - lib/openreceive/rails/version.rb
131
125
  - lib/openreceive/reconcile.rb
132
126
  - lib/openreceive/reconcile_scan.rb
127
+ - lib/rails/commands/openreceive/openreceive_command.rb
133
128
  - lib/tasks/openreceive.rake
134
- - skills/debug-openreceive-payment/SKILL.md
135
- - skills/integrate-openreceive/SKILL.md
136
- - skills/integrate-openreceive/references/btcpay.md
137
- - skills/integrate-openreceive/references/django.md
138
- - skills/integrate-openreceive/references/fastapi.md
139
- - skills/integrate-openreceive/references/fastify.md
140
- - skills/integrate-openreceive/references/laravel.md
141
- - skills/integrate-openreceive/references/next.md
142
- - skills/integrate-openreceive/references/node.md
143
- - skills/integrate-openreceive/references/php.md
144
- - skills/integrate-openreceive/references/rails.md
145
- - skills/integrate-openreceive/references/woocommerce.md
146
129
  homepage: https://openreceive.org
147
130
  licenses:
148
131
  - MIT
@@ -1,97 +0,0 @@
1
- ---
2
- name: debug-openreceive-payment
3
- description: >
4
- Diagnose a failing OpenReceive integration. Use when an OpenReceive-powered
5
- checkout misbehaves: the server refuses to boot, checkout routes return 403,
6
- 404, 409, or 5xx, a paid invoice never settles, a swap refund seems
7
- unreachable, or the checkout UI renders nothing.
8
- license: MIT
9
- ---
10
-
11
- # Debug an OpenReceive payment
12
-
13
- Work top-down: configuration, then the request, then settlement. Every guide
14
- URL below is raw markdown — fetch it when the step needs it.
15
-
16
- ## 1. Run the doctor first
17
-
18
- ```sh
19
- npx openreceive doctor # Node version, NWC_URI, swap config, wallet probe
20
- npx openreceive doctor --db <db> # + are openreceive_payments/openreceive_meta migrated?
21
- npx openreceive doctor --url http://localhost:3000 # + are the routes actually mounted?
22
- ```
23
-
24
- Each failing line states its own fix. `npx openreceive debug-report` prints the
25
- same diagnostics redacted, always exit 0 — safe to share.
26
-
27
- ## 2. Boot failures
28
-
29
- | Symptom | Cause and fix |
30
- | --- | --- |
31
- | `MISSING_NWC` / "needs a receive-only NWC code" | `NWC_URI` is not in the server process env. A `.env` file alone is not enough — something must load it (`dotenv/config`, Next auto-load). Get a code: https://openreceive.org/get_a_nwc_code_to_receive_payments |
32
- | `INVALID_NWC` / "not a valid NWC code" | The value is malformed (must be `nostr+walletconnect://` with 64-hex pubkey and secret, ≥1 `wss` relay). Re-copy it from the wallet. |
33
- | "NOT receive-only" / spend methods advertised | The wallet minted a spend-capable code; OpenReceive fails closed because a leak would drain the wallet. Mint a receive-only code. Overriding (`allowSpendCapableWallet` / `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC`) is a last resort. |
34
- | Wallet preflight failed (methods/encryption) | The wallet must advertise `make_invoice` + `list_transactions` and NIP-04 or NIP-44 v2. Use a compatible wallet. |
35
- | "The openreceive_meta table does not exist" / raw `no such table: openreceive_payments` | The migration was never applied. Node: `npx openreceive scaffold payments --orm <yours>`, then run the emitted migration through the app's normal workflow. Rails: `bin/rails generate openreceive:install`, then `bin/rails db:migrate`. https://openreceive.org/guides/storage.md |
36
- | "requires amountFor / onPaid / authorize / host" | The factory is missing a required hook — see the host contract in https://openreceive.org/guides/api-reference.md |
37
-
38
- ## 3. Request-time errors from the routes
39
-
40
- | Status | Meaning | Where to look |
41
- | --- | --- | --- |
42
- | 403 FORBIDDEN | Your own `authorize` hook denied it, or the request looked cross-site. Check the session/cookie actually reaches the checkout routes. https://openreceive.org/guides/authorization.md |
43
- | Never 403s — any visitor can mint, poll, or refund for any reference | The opposite failure: on Rails the generated `config.authorize = OpenReceive::ALLOW_ALL_AUTHORIZE` placeholder is still installed (the engine warns at boot; `bin/rails openreceive:doctor` reports it). Replace it with the app's real ownership check. https://openreceive.org/guides/authorization.md |
44
- | 404 NOT_FOUND | `amountFor` returned `null` (unknown reference), or the `payment_hash` does not belong to that reference. |
45
- | 409 CONFLICT | **Normal state, not a bug**: the reference already settled, or an unpaid checkout for that method is already live. Show it as order state; never retry-loop. |
46
- | 503 retryable | The host hook failed while persisting the attempt (instructions withheld), or the wallet is unavailable. Read the server log for the underlying error. |
47
- | Framework 404 / HTML error page | The router is not mounted, or mounted at a different prefix than the UI's `prefix` prop. `doctor --url` distinguishes these. |
48
-
49
- ## 4. Paid but never settles
50
-
51
- - Settlement is opportunistic: any OpenReceive request runs one reconcile pass
52
- through a durable gate (min 3s between wallet scans, stretched by invoice
53
- age). A quiet server settles on the next request — or run the optional
54
- notification worker. No timer is missing; that is the design.
55
- - An unpaid attempt closes only after a successful wallet scan at/after expiry
56
- plus a 900s grace constant — a local clock alone never closes one. `expired`
57
- arriving "late" is correct.
58
- - `onPaid` runs once per reference, first settled attempt only, inside the
59
- settlement transaction. If your fulfillment did not run, check whether the
60
- guarded `UPDATE … WHERE` matched zero rows (already transitioned).
61
- https://openreceive.org/guides/storage.md
62
-
63
- ## 5. Swaps and refunds
64
-
65
- - A deposit that arrives short or late becomes `refund_required`; the payer
66
- claims it on a second visit. That needs a per-order URL you serve
67
- (`/checkout/:reference`, `syncUrl` on the drop-ins). Keep the
68
- `payment_hash`: `POST /swaps/status` reopens the attempt with no expiry
69
- window, while re-picking the coin mints a new deposit after ~30 minutes.
70
- - Refunds exist only for swap deposits from `refund_required`. There is **no
71
- Lightning refund** — the wallet cannot spend. Do not chase one.
72
- https://openreceive.org/guides/swap-refunds.md
73
- - "Payer reports two different amounts on a stablecoin checkout" (50.05 or
74
- 50.03?): the deposit amount is a token quantity, `fee.pay_in_fiat` is its
75
- fiat valuation. Only `swap.deposit_amount` is an instruction. From 0.4.10 the
76
- packaged checkout renders a USD stablecoin's breakdown in the token and never
77
- shows `pay_in_fiat`; on an older bundle, upgrade `@openreceive/*`. To verify,
78
- read the row's `deposit_amount` and `fee` and confirm the UI shows only the
79
- deposit amount. A custom UI must call `createSwapFeeBreakdown(fee, swap)`
80
- with the swap, not the fee alone.
81
-
82
- ## 6. Checkout UI shows nothing
83
-
84
- - The components require `prefix` — the exact base path the routes are mounted
85
- at (`"/openreceive"` unless you changed it).
86
- - Import the stylesheet (`@openreceive/react/styles.css` or the elements
87
- sheet).
88
- - "invoice must not be an NWC connection string" means a server secret leaked
89
- into a browser payload — stop and fix the server response; never render it.
90
- https://openreceive.org/guides/frontend-checkout.md
91
-
92
- ## Still stuck
93
-
94
- The full route/option/error reference:
95
- https://openreceive.org/guides/api-reference.md · machine-readable contract:
96
- https://openreceive.org/openapi.yaml · library bug reports:
97
- https://openreceive.org/contact
@@ -1,139 +0,0 @@
1
- ---
2
- name: integrate-openreceive
3
- description: >
4
- Integrate OpenReceive inbound Bitcoin Lightning payments into an application.
5
- Use when adding Bitcoin, Lightning, or crypto checkout to a Node.js, Express,
6
- Fastify, Next.js, Rails, React, Vue, Svelte, Angular, or plain-HTML
7
- application with OpenReceive (the @openreceive/* npm packages or the
8
- openreceive-rails gem), or when connecting a BTCPay Server store to a
9
- receive-only NWC wallet with the OpenReceive plugin.
10
- license: MIT
11
- ---
12
-
13
- # Integrate OpenReceive
14
-
15
- OpenReceive is a payment library that runs inside the application you are
16
- editing. It mounts HTTP routes there, issues Lightning invoices against a
17
- wallet the merchant already controls, and calls back into your code when one
18
- settles. There is no OpenReceive account and no API key; funds land directly in
19
- the merchant's wallet. The one required credential is a **receive-only NWC
20
- code** (`NWC_URI`).
21
-
22
- ## Pick the stack, then follow its directions
23
-
24
- 1. Identify the server stack of the application you are in.
25
- 2. Open the matching reference — it is complete (quickstart inlined) and needs
26
- no network access:
27
- - Node, Express: [references/node.md](references/node.md)
28
- - Node, Fastify: [references/fastify.md](references/fastify.md)
29
- - Node, Next.js App Router: [references/next.md](references/next.md)
30
- - Rails: [references/rails.md](references/rails.md)
31
- - Django: [references/django.md](references/django.md)
32
- - Laravel: [references/laravel.md](references/laravel.md)
33
- - WordPress + WooCommerce: [references/woocommerce.md](references/woocommerce.md) — the packaged gateway and merchant settings.
34
- - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
35
- configured in BTCPay's store UI or Greenfield API; no application code,
36
- no npm packages, no gem. The rest of this file is about the library.
37
- 3. Follow its **Step 0** first: before writing code or searching the machine,
38
- ask the user for the receive-only NWC code (then the swap URI), one question
39
- per message, and store each pasted code in the project's env file yourself.
40
- Never print the value; never invent a placeholder.
41
-
42
- Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
43
- Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js:
44
- `npm install @openreceive/next @openreceive/react`. Swap the UI package (`vue`,
45
- `svelte`, `angular`, `elements`) for the frontend the app already has. Install
46
- (Rails): `bundle add openreceive-rails`.
47
-
48
- ## The three server objects
49
-
50
- | Object | Built with | Talks to |
51
- | --- | --- | --- |
52
- | Wallet client | `createOpenReceive()` | the merchant's wallet — mints invoices, reads settlement, holds the NWC code |
53
- | Host | `createHost()` | your database — your hooks plus the `openreceive_payments` table |
54
- | HTTP routes | `openReceiveExpress()` / `openReceiveFastify()` / `openReceiveNext()` / the Rails engine | the browser — mounted at `/openreceive` by default |
55
-
56
- The quickstart's one-factory form (`openReceiveExpress({ wallet, storage,
57
- amountFor, authorize })`) builds all three; compose them separately only for a
58
- shared wallet client or a custom repository. The checkout UI
59
- (`<Checkout reference={...} prefix="/openreceive" />`) is the optional fourth
60
- piece.
61
-
62
- ## The host contract: authorize, amountFor, onPaid
63
-
64
- Your application keeps orders, users, prices, and fulfillment. Three hooks are
65
- the entire bridge — wire them to the models this app already has, never to
66
- copied demo models:
67
-
68
- - `amountFor(reference)` — the authoritative price, read from your own data.
69
- Return `{ currency, value, description }` with `value` a **decimal string**
70
- (never a float, never payer input), or `null` when there is nothing to pay
71
- for. The `reference` is your order id: one per thing you fulfill, created
72
- before checkout, kept across retries, never reused.
73
- - `authorize({ action, request, resource })` — your own access check, run on
74
- every request. `resource.reference` is a claim the payer made, not proof;
75
- read a real session.
76
- - `onPaid({ reference, paidAt, query })` — fulfillment, run once per reference
77
- inside the settlement transaction, only for the first settled attempt. Use
78
- the provided `query`, not your ORM's other connection, and guard the
79
- transition (`UPDATE … WHERE state = 'awaiting_payment'`).
80
-
81
- ## 409 is a state, not a failure
82
-
83
- The library serializes attempts per reference. A create that returns **409
84
- CONFLICT** is normal checkout flow: the reference already settled, or an unpaid
85
- checkout for that payment method is already in progress. Surface it as order
86
- state; do not retry-loop it, and do not build an idempotency store around it —
87
- that serialization is the library's job. (A hook failure while persisting an
88
- attempt is a **503 retryable**, deliberately distinct.)
89
-
90
- ## Amounts on the deposit panel
91
-
92
- `swap.deposit_amount` is the ONLY amount a payer is ever told to send, in the
93
- pay-in token. `swap.fee.pay_in_fiat` / `payout_fiat` are fiat valuations that
94
- explain the spread (why the deposit exceeds the cart total); they are not
95
- instructions. For a stablecoin pegged to the fee currency (USDT, USDC) the
96
- packaged checkout expresses the breakdown in the token and never renders
97
- `pay_in_fiat` — "$50.03" under "50.05 USDC" reads as the same number with a
98
- typo. A custom UI gets the same rule from `createSwapFeeBreakdown(fee, swap)`;
99
- pass the swap, not just the fee.
100
-
101
- ## Secrets
102
-
103
- `NWC_URI` and `LSC_URI_*` are server-only. Never put them in browser code,
104
- logs, assets, or tests. Boot fails closed if the NWC code advertises spend
105
- methods such as `pay_invoice` — mint a receive-only code
106
- (https://openreceive.org/get_a_nwc_code_to_receive_payments) instead of
107
- overriding.
108
-
109
- ## Database tables
110
-
111
- ```sh
112
- npx openreceive scaffold payments --orm prisma # or drizzle | typeorm | sequelize | knex
113
- ```
114
-
115
- emits the `openreceive_payments` + `openreceive_meta` migration for THIS app's
116
- database (Rails: `bin/rails generate openreceive:install`); run it through the
117
- app's normal migration workflow. The tables sit beside your models — no
118
- relations to them, no separate database, no Redis.
119
-
120
- ## Verify, and test without a real wallet
121
-
122
- `npx openreceive doctor` checks the configuration and says what to fix.
123
-
124
- For tests, inject a fake wallet at the stable seams — `client` on
125
- `createOpenReceive` (any object with `preflight`, `makeInvoice`,
126
- `listTransactions`) or `config.nwc_client` in Rails — plus
127
- `StaticPriceProvider` for fiat pricing without a network. Your routes,
128
- persistence, reconcile, and `onPaid` then run the production code paths.
129
- Details: https://openreceive.org/guides/host-testing.md
130
-
131
- ## Deeper documentation
132
-
133
- Fetch on demand — each URL is raw markdown:
134
- https://openreceive.org/guides/authorization.md ·
135
- https://openreceive.org/guides/storage.md ·
136
- https://openreceive.org/guides/api-reference.md ·
137
- https://openreceive.org/guides/security.md ·
138
- https://openreceive.org/openapi.yaml (the normative HTTP contract) ·
139
- https://openreceive.org/llms.txt (the full index)