@forgesworn/moneyer 0.12.0 → 0.13.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1 - 2026-09-11
4
+
5
+ - A name's owner can set or clear its `cx1` while self-service registration
6
+ is closed. Closing registration stops new names; it no longer stops the
7
+ owner of an existing one, an operator name included, from choosing where
8
+ it pays. A request for a new name still gets the same 404.
9
+
10
+ ## 0.13.0 - 2026-09-11
11
+
12
+ - **Names that pay to the holder's own keys.** `POST /names` takes a `cx1`,
13
+ a watch-only branch, and the key that owns a name can set or clear one
14
+ later. A zap to such a name is credited to the holder's next key on the
15
+ branch, and the gift wrap carries only where to look and the index:
16
+ `/w?p=<cp1>&amount=&sig=<cs1>&i=`, plus an `i` tag. The mint never holds
17
+ a secret for it.
18
+ - The index is taken when the zap settles, never when the invoice is made,
19
+ so an unpaid invoice leaves no gap in the holder's scan. A key that
20
+ already holds a note is skipped.
21
+ - Names gain two columns, `cx1` and `next_index`, added in place on first
22
+ start.
23
+
3
24
  ## 0.12.0 - 2026-09-11
4
25
 
5
26
  - LUD-25's renamed parameters are accepted: `p` for the informational
package/README.md CHANGED
@@ -542,6 +542,31 @@ A registered name resolves on both rails at once:
542
542
  The discovery endpoint advertises `namePriceMsat` while registration is
543
543
  open, so a wallet can offer the flow without asking.
544
544
 
545
+ ### A name that pays to your own keys (LUD-25 Part 2)
546
+
547
+ Add `"cx1": "cx1..."` to the registration body, or send `{"name", "cx1"}`
548
+ again later from the key that owns the name, and payments to it stop being
549
+ custodial. A `cx1` is a watch-only branch: from it the mint can work out each
550
+ of the holder's note keys in turn, but never spend one.
551
+
552
+ When a zap to the name settles, the mint takes the next index on the branch
553
+ and credits the note to that key. It skips any key already holding a note,
554
+ and an invoice nobody pays takes no index at all, so a wallet scanning its
555
+ branch never meets a gap it did not make. The gift wrap still goes to the
556
+ owner's npub, but it carries no secret: a lookup URL,
557
+ `https://mint.example/w?p=<cp1>&amount=<msat>&sig=<cs1>&i=<index>`, plus an
558
+ `i` tag. The wallet derives the key at that index, checks the certificate,
559
+ and spends with its own `ck1`. A wallet that only knows note URLs sees no
560
+ `k1` and passes the wrap by, and the note waits at the mint for a scan of the
561
+ branch.
562
+
563
+ `"cx1": null` clears the branch and puts the name back on the custodial path.
564
+ This works while registration is closed too: closing it stops new names, not
565
+ the owner of one choosing where it pays, including a name the operator set up
566
+ in `MONEYER_ZAP_NAMES`.
567
+ Sending a new branch starts it at index 0; sending the same one again keeps
568
+ its place. Zap receipts are unchanged.
569
+
545
570
  ## Reaching the mint over Tor
546
571
 
547
572
  Set `MONEYER_ONION_URL` to this mint's hidden service address and a request
package/THREAT-MODEL.md CHANGED
@@ -90,6 +90,14 @@ only the buyer ever held, the preimage is bearer material for nothing, and
90
90
  what `/verify` serves is an ordinary payment proof. Wallets should prefer
91
91
  that path wherever a mint advertises `mintToHash`.
92
92
 
93
+ **What a `cx1` on a name gives away.** A name with a watch-only branch is
94
+ paid to the holder's keys, so the mint never holds a secret for those notes.
95
+ But the mint can work out every key on the branch, so it can link every
96
+ payment to that name, and see when each note is spent. That is no more than
97
+ a custodial name already reveals. A wallet should keep the branch for
98
+ receiving and rotate what arrives onto keys the mint cannot enumerate, which
99
+ is what notecase and lnurl-wallet do.
100
+
93
101
  ## Out of scope
94
102
 
95
103
  - Compromise of the host or the funding source credentials: at that point
package/dist/names.d.ts CHANGED
@@ -24,6 +24,8 @@ export type NameGranted = {
24
24
  name: string;
25
25
  pubkey: string;
26
26
  paidMsat: number;
27
+ cx1: string | null;
28
+ updated: boolean;
27
29
  };
28
30
  export declare const isRefusal: (result: NameGranted | NameRefusal) => result is NameRefusal;
29
31
  export declare const registerName: (args: {
@@ -32,6 +34,7 @@ export declare const registerName: (args: {
32
34
  body: {
33
35
  name?: unknown;
34
36
  note?: unknown;
37
+ cx1?: unknown;
35
38
  };
36
39
  priceMsat: number | undefined;
37
40
  reserved: string[];
package/dist/names.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sha256 } from '@noble/hashes/sha2.js';
2
2
  import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
3
3
  import { verifyEvent } from 'nostr-tools/pure';
4
- import { hashK1, noteK1 } from 'lnurlcash-kit';
4
+ import { decodeCx1, hashK1, noteK1 } from 'lnurlcash-kit';
5
5
  import { NotePendingError, NoteStore, NoteUnavailableError } from "./store.js";
6
6
  // Self-service lightning addresses.
7
7
  //
@@ -94,8 +94,6 @@ export const isRefusal = (result) => 'reason' in result;
94
94
  // the same name at once.
95
95
  export const registerName = (args) => {
96
96
  const { store, pubkey, priceMsat } = args;
97
- if (priceMsat === undefined)
98
- return refuse('This mint is not registering names.', 404);
99
97
  const raw = typeof args.body.name === 'string' ? args.body.name.trim().toLowerCase() : '';
100
98
  if (!NAME_RULE.test(raw)) {
101
99
  return refuse('A name is 3 to 32 characters of a-z, 0-9, dot, dash or underscore, starting with a letter or digit.', 400);
@@ -103,8 +101,31 @@ export const registerName = (args) => {
103
101
  if ([...ALWAYS_RESERVED, ...args.reserved.map(name => name.toLowerCase())].includes(raw)) {
104
102
  return refuse('That name is reserved.', 403);
105
103
  }
106
- if (store.zapName(raw))
104
+ // LUD-25 Part 2: a watch-only branch, so payments to the name are minted
105
+ // to the holder's own keys. Absent leaves it as it is; null clears it.
106
+ let cx1;
107
+ if (args.body.cx1 === null) {
108
+ cx1 = null;
109
+ }
110
+ else if (args.body.cx1 !== undefined) {
111
+ const given = typeof args.body.cx1 === 'string' ? args.body.cx1.trim().toLowerCase() : '';
112
+ if (!decodeCx1(given))
113
+ return refuse('That is not a cx1.', 400);
114
+ cx1 = given;
115
+ }
116
+ const existing = store.zapName(raw);
117
+ if (existing) {
118
+ // Its owner may set or clear the branch; nobody else may touch it.
119
+ if (existing.pubkey === pubkey && cx1 !== undefined) {
120
+ store.setZapNameCx1(raw, cx1);
121
+ return { name: raw, pubkey, paidMsat: 0, cx1, updated: true };
122
+ }
107
123
  return refuse('That name is taken.', 409);
124
+ }
125
+ // Checked after the owner's update above: closing registration stops new
126
+ // names, not a holder choosing where their own name pays.
127
+ if (priceMsat === undefined)
128
+ return refuse('This mint is not registering names.', 404);
108
129
  let paidMsat = 0;
109
130
  let noteId;
110
131
  if (priceMsat === 0) {
@@ -157,7 +178,7 @@ export const registerName = (args) => {
157
178
  paidMsat = note.amountMsat;
158
179
  }
159
180
  try {
160
- store.buyZapName({ name: raw, pubkey, ...(noteId ? { noteId } : {}), paidMsat });
181
+ store.buyZapName({ name: raw, pubkey, ...(noteId ? { noteId } : {}), paidMsat, cx1: cx1 ?? null });
161
182
  }
162
183
  catch (err) {
163
184
  if (err instanceof NotePendingError)
@@ -168,5 +189,5 @@ export const registerName = (args) => {
168
189
  // the name and the note together or not at all.
169
190
  return refuse('That name is taken.', 409);
170
191
  }
171
- return { name: raw, pubkey, paidMsat };
192
+ return { name: raw, pubkey, paidMsat, cx1: cx1 ?? null, updated: false };
172
193
  };
package/dist/server.js CHANGED
@@ -167,6 +167,7 @@ export const createMoneyer = async (config, deps = {}) => {
167
167
  mintFeeLine,
168
168
  feeInWords,
169
169
  verify: config.verify,
170
+ certify: (noteId, amountMsat) => certify({ id: noteId, cp1: true }, amountMsat),
170
171
  // configFromEnv guarantees this when zap is set; a caller
171
172
  // building the config by hand gets the same rule.
172
173
  origin: config.publicOrigin ?? (() => {
@@ -448,7 +449,10 @@ export const createMoneyer = async (config, deps = {}) => {
448
449
  // a POST because it creates something, and it is authenticated by
449
450
  // NIP-98 rather than by anything this mint has to store.
450
451
  if (requestUrl.pathname === '/names' && req.method === 'POST') {
451
- if (config.namePriceMsat === undefined)
452
+ // Closed registration still lets a name's owner set where it pays;
453
+ // registerName refuses a new name there. Without zaps there are no
454
+ // names at all.
455
+ if (!zap)
452
456
  return fail('This mint is not registering names.', 404);
453
457
  const body = await readBody(req);
454
458
  if (body === null)
@@ -480,14 +484,19 @@ export const createMoneyer = async (config, deps = {}) => {
480
484
  });
481
485
  if (isRefusal(result))
482
486
  return fail(result.reason, result.status);
483
- log(`name ${result.name} registered to ${result.pubkey.slice(0, 8)} for ${result.paidMsat} msat`);
487
+ log(result.updated
488
+ ? `name ${result.name} ${result.cx1 ? 'now pays to a branch' : 'pays out custodially again'}`
489
+ : `name ${result.name} registered to ${result.pubkey.slice(0, 8)} for ${result.paidMsat} msat`);
484
490
  return send({
485
491
  status: 'OK',
486
492
  name: result.name,
487
493
  pubkey: result.pubkey,
488
494
  address: `${result.name}@${host}`,
489
495
  priceMsat: config.namePriceMsat,
490
- paidMsat: result.paidMsat
496
+ paidMsat: result.paidMsat,
497
+ // With a cx1 the name pays to the holder's own keys; without, as a
498
+ // note gift-wrapped to them.
499
+ cx1: result.cx1
491
500
  });
492
501
  }
493
502
  // Every LNURL endpoint below is a GET. The method matters: /w/cb
package/dist/store.d.ts CHANGED
@@ -53,6 +53,8 @@ export type ZapNameRow = {
53
53
  createdAt: number;
54
54
  paidMsat: number;
55
55
  source: 'env' | 'self';
56
+ cx1: string | null;
57
+ nextIndex: number;
56
58
  };
57
59
  export type NoteListRow = NoteRow & {
58
60
  createdAt: number;
@@ -126,16 +128,27 @@ export declare class NoteStore {
126
128
  unsettledZapInvoices(): ZapInvoiceRow[];
127
129
  deleteUnsettledZapInvoice(paymentHash: string): void;
128
130
  settleZapInvoice(paymentHash: string, noteId: string, wrapJson: string, receiptJson: string | null): boolean;
131
+ settleZapInvoiceToKey(paymentHash: string, keyAt: (index: number) => string | null, build: (noteId: string, index: number) => {
132
+ wrapJson: string;
133
+ receiptJson: string | null;
134
+ }): {
135
+ noteId: string;
136
+ index: number;
137
+ } | null;
129
138
  unpublishedZaps(): ZapInvoiceRow[];
130
139
  markZapPublished(paymentHash: string): void;
140
+ private static readonly NAME_COLUMNS;
141
+ private nameRow;
131
142
  zapName(name: string): ZapNameRow | null;
132
143
  zapNames(): ZapNameRow[];
144
+ setZapNameCx1(name: string, cx1: string | null): void;
133
145
  putOperatorZapName(name: string, pubkey: string): void;
134
146
  buyZapName(args: {
135
147
  name: string;
136
148
  pubkey: string;
137
149
  noteId?: string;
138
150
  paidMsat: number;
151
+ cx1?: string | null;
139
152
  }): void;
140
153
  removeZapName(name: string): boolean;
141
154
  zapNameCountFor(pubkey: string): number;
package/dist/store.js CHANGED
@@ -117,6 +117,12 @@ export class NoteStore {
117
117
  // Two invoices may not name the same note. SQLite counts NULLs as
118
118
  // distinct, so every unbound invoice still fits.
119
119
  this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS mint_invoices_output_id ON mint_invoices (output_id)');
120
+ const nameColumns = this.db.prepare('PRAGMA table_info(zap_names)').all().map(column => column.name);
121
+ if (!nameColumns.includes('cx1'))
122
+ this.db.exec('ALTER TABLE zap_names ADD COLUMN cx1 TEXT');
123
+ if (!nameColumns.includes('next_index')) {
124
+ this.db.exec('ALTER TABLE zap_names ADD COLUMN next_index INTEGER NOT NULL DEFAULT 0');
125
+ }
120
126
  this.db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);');
121
127
  // The moment this mint stopped publishing preimages for notes nobody
122
128
  // named. Written once, the first time a build carrying this code opens
@@ -440,6 +446,39 @@ export class NoteStore {
440
446
  });
441
447
  }
442
448
  // Settled zaps whose wrap has not yet reached a relay.
449
+ // A zap to a name with a cx1, minted to the next unused key on its
450
+ // branch. The index is taken here, at settlement, never when the invoice
451
+ // is made, so an invoice nobody pays leaves no gap in the holder's scan.
452
+ // A key that already names a note is skipped, as LUD-25 requires.
453
+ // `keyAt` answers null for an index that yields no usable key.
454
+ settleZapInvoiceToKey(paymentHash, keyAt, build) {
455
+ return this.tx(() => {
456
+ const row = this.zapInvoiceByHash(paymentHash);
457
+ if (!row || row.settled)
458
+ return null;
459
+ const name = this.zapName(row.name);
460
+ if (!name?.cx1)
461
+ return null;
462
+ let index = name.nextIndex;
463
+ let noteId = null;
464
+ for (let tries = 0; tries < 1000 && noteId === null; tries++) {
465
+ const candidate = keyAt(index);
466
+ if (candidate !== null && !this.outputIdInUse(candidate))
467
+ noteId = candidate;
468
+ else
469
+ index += 1;
470
+ }
471
+ if (noteId === null)
472
+ throw new Error(`no free key on the branch of ${row.name}`);
473
+ this.insertNote(noteId, row.netMsat);
474
+ this.db.prepare('UPDATE zap_names SET next_index = ? WHERE name = ?').run(index + 1, row.name);
475
+ const { wrapJson, receiptJson } = build(noteId, index);
476
+ this.db
477
+ .prepare('UPDATE zap_invoices SET settled = 1, note_id = ?, wrap_json = ?, receipt_json = ?, settled_at = ? WHERE payment_hash = ?')
478
+ .run(noteId, wrapJson, receiptJson, Date.now(), paymentHash);
479
+ return { noteId, index };
480
+ });
481
+ }
443
482
  unpublishedZaps() {
444
483
  const rows = this.db
445
484
  .prepare(`SELECT ${NoteStore.ZAP_COLUMNS} FROM zap_invoices WHERE settled = 1 AND published_at IS NULL`)
@@ -458,25 +497,36 @@ export class NoteStore {
458
497
  // One table for both kinds: names the operator set in the environment
459
498
  // and names people registered themselves. The lookup path that serves a
460
499
  // zap does not care which is which, so there is one of it.
500
+ static NAME_COLUMNS = 'name, pubkey, created_at, paid_msat, source, cx1, next_index';
501
+ nameRow(row) {
502
+ return {
503
+ name: row.name,
504
+ pubkey: row.pubkey,
505
+ createdAt: row.created_at,
506
+ paidMsat: row.paid_msat,
507
+ source: row.source,
508
+ cx1: row.cx1 ?? null,
509
+ nextIndex: row.next_index
510
+ };
511
+ }
461
512
  zapName(name) {
462
513
  const row = this.db
463
- .prepare('SELECT name, pubkey, created_at, paid_msat, source FROM zap_names WHERE name = ?')
514
+ .prepare(`SELECT ${NoteStore.NAME_COLUMNS} FROM zap_names WHERE name = ?`)
464
515
  .get(name.toLowerCase());
465
- if (!row)
466
- return null;
467
- return { name: row.name, pubkey: row.pubkey, createdAt: row.created_at, paidMsat: row.paid_msat, source: row.source };
516
+ return row ? this.nameRow(row) : null;
468
517
  }
469
518
  zapNames() {
470
519
  const rows = this.db
471
- .prepare('SELECT name, pubkey, created_at, paid_msat, source FROM zap_names ORDER BY name')
520
+ .prepare(`SELECT ${NoteStore.NAME_COLUMNS} FROM zap_names ORDER BY name`)
472
521
  .all();
473
- return rows.map(row => ({
474
- name: row.name,
475
- pubkey: row.pubkey,
476
- createdAt: row.created_at,
477
- paidMsat: row.paid_msat,
478
- source: row.source
479
- }));
522
+ return rows.map(row => this.nameRow(row));
523
+ }
524
+ // A new branch starts at index 0; the same branch sent again keeps its
525
+ // place, so re-registering cannot walk the holder's keys backwards.
526
+ setZapNameCx1(name, cx1) {
527
+ this.db
528
+ .prepare('UPDATE zap_names SET next_index = CASE WHEN cx1 IS ? THEN next_index ELSE 0 END, cx1 = ? WHERE name = ?')
529
+ .run(cx1, cx1, name.toLowerCase());
480
530
  }
481
531
  // The operator's own names, re-applied at every startup. Idempotent, and
482
532
  // the environment wins: it is the operator's mint, and a name they put
@@ -499,8 +549,8 @@ export class NoteStore {
499
549
  this.setNoteState(args.noteId, 'burned');
500
550
  }
501
551
  this.db
502
- .prepare("INSERT INTO zap_names (name, pubkey, created_at, paid_msat, source) VALUES (?, ?, ?, ?, 'self')")
503
- .run(args.name.toLowerCase(), args.pubkey, Date.now(), args.paidMsat);
552
+ .prepare("INSERT INTO zap_names (name, pubkey, created_at, paid_msat, source, cx1) VALUES (?, ?, ?, ?, 'self', ?)")
553
+ .run(args.name.toLowerCase(), args.pubkey, Date.now(), args.paidMsat, args.cx1 ?? null);
504
554
  });
505
555
  }
506
556
  removeZapName(name) {
package/dist/zap.d.ts CHANGED
@@ -31,6 +31,7 @@ export type ZapBridgeDeps = {
31
31
  feeInWords: string | null;
32
32
  verify: boolean;
33
33
  origin: string;
34
+ certify: (noteIdHex: string, amountMsat: number) => string;
34
35
  log?: (message: string) => void;
35
36
  now?: () => number;
36
37
  };
package/dist/zap.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
2
- import { hashK1 } from 'lnurlcash-kit';
2
+ import { decodeCx1, deriveNotePubkey, encodeCp1, hashK1 } from 'lnurlcash-kit';
3
3
  import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
4
4
  import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
5
5
  import { SimplePool } from 'nostr-tools/pool';
@@ -171,8 +171,7 @@ export const createZapBridge = (deps) => {
171
171
  store.recordZapInvoice({ paymentHash, name: lowered, recipient, pr, grossMsat: amountMsat, netMsat: net, zapRequest });
172
172
  return { pr, ...(deps.verify ? { verify: `${origin}/verify/${paymentHash}` } : {}) };
173
173
  };
174
- // The note as the recipient's wallet will paste it, wrapped to them.
175
- const buildWrap = (row, k1) => {
174
+ const wrapTags = (row) => {
176
175
  const tags = [
177
176
  ['p', row.recipient],
178
177
  ['amount', String(row.netMsat)],
@@ -192,15 +191,20 @@ export const createZapBridge = (deps) => {
192
191
  // older one simply does not see this.
193
192
  tags.push(['description', row.zapRequest]);
194
193
  }
195
- const rumor = {
196
- kind: NOTE_KIND,
197
- pubkey,
198
- created_at: Math.floor(now() / 1000),
199
- tags,
200
- content: `${origin}/w?k1=${k1}&amount=${row.netMsat}`
201
- };
194
+ return tags;
195
+ };
196
+ const wrap = (row, tags, content) => {
197
+ const rumor = { kind: NOTE_KIND, pubkey, created_at: Math.floor(now() / 1000), tags, content };
202
198
  return wrapEvent(rumor, secret, row.recipient);
203
199
  };
200
+ // The note as the recipient's wallet will paste it, wrapped to them.
201
+ const buildWrap = (row, k1) => wrap(row, wrapTags(row), `${origin}/w?k1=${k1}&amount=${row.netMsat}`);
202
+ // A zap to a name with a cx1: the note already belongs to the holder's
203
+ // key, so the wrap carries no secret at all, only where to look (`p`),
204
+ // the certificate, and the index `i` the holder derives the key at. A
205
+ // reader that knows only note URLs sees no k1 and passes it by, and the
206
+ // note waits at the mint for a scan to find it.
207
+ const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&amount=${row.netMsat}&sig=${deps.certify(noteId, row.netMsat)}&i=${index}`);
204
208
  // NIP-57 receipt, without the preimage tag: it is optional there, and
205
209
  // here it would only invite someone to mistake it for the note.
206
210
  const buildReceipt = (row) => {
@@ -222,6 +226,27 @@ export const createZapBridge = (deps) => {
222
226
  }
223
227
  if (!paid)
224
228
  continue;
229
+ const branch = decodeCx1(store.zapName(row.name)?.cx1 ?? '');
230
+ if (branch) {
231
+ const receipt = buildReceipt(row);
232
+ const keyAt = (index) => {
233
+ try {
234
+ return bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, index));
235
+ }
236
+ catch {
237
+ return null;
238
+ }
239
+ };
240
+ const landed = store.settleZapInvoiceToKey(row.paymentHash, keyAt, (noteId, index) => ({
241
+ wrapJson: JSON.stringify(buildKeyWrap(row, noteId, index)),
242
+ receiptJson: receipt ? JSON.stringify(receipt) : null
243
+ }));
244
+ if (landed) {
245
+ minted += 1;
246
+ log(`zap: ${row.name} received ${row.netMsat} msat; note minted to key ${landed.index}, wrap parked`);
247
+ }
248
+ continue;
249
+ }
225
250
  const k1 = bytesToHex(randomBytes(32));
226
251
  const noteId = hashK1(k1);
227
252
  const wrap = buildWrap(row, k1);
package/llms.txt CHANGED
@@ -91,3 +91,9 @@ NoteStore - SQLite store; notes by sha256(k1), never secrets
91
91
  /verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
92
92
  /w LUD-03 informational GET by k1 (secret or ck1) or p (hash or cp1, older h); a cp1 note carries its cs1 as sig
93
93
  /w/cb melt / rotate / split / merge; inputs secrets or ck1, outputs p1/p2 hashes or cp1 (or older h/h2); cp1 outputs certified as cs1
94
+ POST /names claim a name (NIP-98; the signer owns it); body {name, note?, cx1?}.
95
+ The owner may resend {name, cx1} to set a branch, or cx1: null to clear it
96
+ /z/cb/{name} zap invoice for a name. With a cx1 the settled note is credited to the
97
+ holder's next key, and the kind 2525 gift wrap carries no secret:
98
+ /w?p=<cp1>&amount=&sig=<cs1>&i=<index> plus an `i` tag. The index is
99
+ taken at settlement, never at the invoice, and occupied keys are skipped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",