402-mcp 4.1.1 → 4.2.0

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.
@@ -0,0 +1,102 @@
1
+ import { createNoteOps, noteUrl, NoteGone } from '../wallet/lnurlcash-ops.js';
2
+ /**
3
+ * Mints arrive as bare hosts (`mint.example.com`), origins, or full discovery
4
+ * URLs. Compared on host so all three forms match a stored note's origin.
5
+ */
6
+ export function mintHost(value) {
7
+ const trimmed = value.trim();
8
+ if (trimmed.length === 0)
9
+ return null;
10
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
11
+ ? trimmed.replace(/^lnurlw:\/\//i, 'https://')
12
+ : `https://${trimmed}`;
13
+ try {
14
+ const host = new URL(withScheme).host;
15
+ return host.length > 0 ? host.toLowerCase() : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Builds a bearer-note payment for an lnurlcash challenge: an exact-value note
23
+ * from an accepted mint, or one split at the mint to the exact price so the
24
+ * paywall is never overpaid and the change stays in the store. Returns null
25
+ * when no note can cover it, so the caller falls through to another rail.
26
+ *
27
+ * The chosen note is held pending before it is returned. Handing a bearer note
28
+ * to a server puts it beyond our control: the server settles by rotating it,
29
+ * and only the mint knows whether that happened.
30
+ */
31
+ export async function attemptLnurlcashPayment(opts) {
32
+ const { challenge, noteStore } = opts;
33
+ const ops = opts.ops ?? createNoteOps(noteStore, opts.options ?? {});
34
+ const accepted = new Set(challenge.mints.map(mintHost).filter((h) => h !== null));
35
+ if (accepted.size === 0)
36
+ return null;
37
+ const needMsat = challenge.amount * 1000;
38
+ try {
39
+ await ops.reconcile();
40
+ }
41
+ catch { /* best effort; selection below simply sees fewer notes */ }
42
+ const live = noteStore.live().filter(n => {
43
+ const host = mintHost(n.mint);
44
+ return host !== null && accepted.has(host);
45
+ });
46
+ // Exact-value notes first: handing one over avoids a split round trip
47
+ // entirely. Then the smallest note that covers the price, so large notes
48
+ // stay whole.
49
+ const candidates = [
50
+ ...live.filter(n => n.amountMsat === needMsat),
51
+ ...live.filter(n => n.amountMsat > needMsat).sort((a, b) => a.amountMsat - b.amountMsat),
52
+ ];
53
+ for (const candidate of candidates) {
54
+ // The stored value is a cache. Only the mint knows what a note is worth
55
+ // now, and a bearer note anyone else holds a copy of may already be spent,
56
+ // so it is confirmed before it is handed over.
57
+ const probe = await ops.probeNote(candidate.mint, candidate.secret);
58
+ if (!probe.ok) {
59
+ // A mint that refuses the note has retired it: drop it and try the next.
60
+ // A mint that cannot be reached has said nothing, so nothing is dropped
61
+ // and no further note is risked against it.
62
+ if (probe.reason === 'unreachable')
63
+ return null;
64
+ noteStore.remove(candidate.secret);
65
+ continue;
66
+ }
67
+ if (probe.note.amountMsat !== candidate.amountMsat) {
68
+ noteStore.setAmount(candidate.secret, probe.note.amountMsat);
69
+ }
70
+ if (probe.note.amountMsat < needMsat)
71
+ continue;
72
+ const current = noteStore.find(candidate.secret);
73
+ if (!current)
74
+ continue;
75
+ let exact;
76
+ try {
77
+ exact = probe.note.amountMsat === needMsat
78
+ ? current
79
+ : await ops.split(current, needMsat);
80
+ }
81
+ catch (error) {
82
+ // A note the mint no longer recognises is normal for a bearer asset:
83
+ // drop it and try the next. Anything else means the mint is refusing or
84
+ // unreachable, so stop rather than burn more notes against it.
85
+ if (error instanceof NoteGone)
86
+ continue;
87
+ return null;
88
+ }
89
+ noteStore.setState(exact.secret, 'melting');
90
+ const secret = exact.secret;
91
+ return {
92
+ header: noteUrl(exact),
93
+ amountSats: challenge.amount,
94
+ settle(granted) {
95
+ if (granted)
96
+ noteStore.remove(secret);
97
+ },
98
+ };
99
+ }
100
+ return null;
101
+ }
102
+ //# sourceMappingURL=payment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.js","sourceRoot":"","sources":["../../src/xlnurlcash/payment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAqC,MAAM,4BAA4B,CAAA;AAiBhH;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACrC,MAAM,UAAU,GAAG,0BAA0B,CAAC,IAAI,CAAC,OAAO,CAAC;QACzD,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,UAAU,CAAC;QAC9C,CAAC,CAAC,WAAW,OAAO,EAAE,CAAA;IACxB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAA;QACrC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,IAK7C;IACC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAEpE,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CACrE,CAAA;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEpC,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,IAAI,CAAA;IAExC,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,SAAS,EAAE,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC,CAAC,0DAA0D,CAAC,CAAC;IAEtE,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;QACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAC7B,OAAO,IAAI,KAAK,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,sEAAsE;IACtE,yEAAyE;IACzE,cAAc;IACd,MAAM,UAAU,GAAG;QACjB,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC;QAC9C,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;KACzF,CAAA;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,wEAAwE;QACxE,2EAA2E;QAC3E,+CAA+C;QAC/C,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;QACnE,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;YACd,yEAAyE;YACzE,wEAAwE;YACxE,4CAA4C;YAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,aAAa;gBAAE,OAAO,IAAI,CAAA;YAC/C,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;YAClC,SAAQ;QACV,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,UAAU,EAAE,CAAC;YACnD,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAC9D,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,QAAQ;YAAE,SAAQ;QAE9C,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QAChD,IAAI,CAAC,OAAO;YAAE,SAAQ;QAEtB,IAAI,KAAK,CAAA;QACT,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,QAAQ;gBACxC,CAAC,CAAC,OAAO;gBACT,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qEAAqE;YACrE,wEAAwE;YACxE,+DAA+D;YAC/D,IAAI,KAAK,YAAY,QAAQ;gBAAE,SAAQ;YACvC,OAAO,IAAI,CAAA;QACb,CAAC;QAED,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;QAE3B,OAAO;YACL,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC;YACtB,UAAU,EAAE,SAAS,CAAC,MAAM;YAC5B,MAAM,CAAC,OAAgB;gBACrB,IAAI,OAAO;oBAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACvC,CAAC;SACF,CAAA;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
package/llms-full.txt CHANGED
@@ -85,6 +85,11 @@ src/
85
85
  xcashu/
86
86
  parse.ts # Detect X-Cashu header; parse NUT-18 payment request
87
87
  payment.ts # Autonomous ecash payment: coin-select, send exact token amount
88
+ xlnurlcash/
89
+ parse.ts # Detect X-LNURLcash header; parse the lnurlcash payment request
90
+ # (v/id/amount-as-string/currency/methodDetails.mints, per the
91
+ # conformance vectors; the older short form is still read)
92
+ payment.ts # Autonomous bearer-note payment: pick or split a note to the exact price
88
93
  ietf-payment/
89
94
  parse.ts # Detect/parse WWW-Authenticate: Payment challenges (draft-ryan-httpauth-payment-01)
90
95
  credential.ts # Build Authorization: Payment base64url credential after Lightning payment
@@ -277,25 +282,27 @@ The L402 protocol is an extension of HTTP 402 (Payment Required):
277
282
 
278
283
  ## Supported 402 Protocol Variants
279
284
 
280
- `l402-fetch` detects and handles four HTTP 402 challenge variants. Detection order in source: L402, x402, xCashu, IETF Payment.
285
+ `l402-fetch` detects and handles five HTTP 402 challenge variants. Detection order in source: LNURLcash, x402, IETF Payment, L402, with xCashu tried immediately after LNURLcash.
281
286
 
282
287
  | Protocol | Detection | Credential sent | Autonomous? |
283
288
  |----------|-----------|----------------|-------------|
284
289
  | **L402** | `WWW-Authenticate: L402 macaroon=…, invoice=…` | `Authorization: L402 <macaroon>:<preimage>` | Yes (NWC or Cashu) |
285
290
  | **IETF Payment** (`draft-ryan-httpauth-payment-01`) | `WWW-Authenticate: Payment id=… method=lightning …` | `Authorization: Payment <base64url-json>` | Yes (NWC or Cashu) |
291
+ | **LNURLcash** (LUD-25) | `X-LNURLcash: lnurlcashreq1<base64url>` | `X-LNURLcash: <bearer note URL>` | Yes (note store only) |
286
292
  | **xCashu** (NUT-18) | `X-Cashu: creqA<base64url>` | `X-Cashu: <cashuB-token>` | Yes (Cashu wallet only) |
287
293
  | **x402** | `X-Payment-Required: x402` + JSON body | Human presents EIP-681 deeplink | No — on-chain EVM; surfaced to human |
288
294
 
289
- xCashu uses coin selection via `wallet.send(amount, proofs, { includeFees: true })` and restores change proofs to the token store. IETF Payment decodes the base64url charge request to extract the BOLT11 invoice, then pays it through the normal wallet stack and wraps the preimage in a JSON credential.
295
+ LNURLcash is tried before the others because a bearer note is already money in hand: the note URL goes straight into the retry header and the server settles it by rotating the note. A note worth exactly the price is used as it is; otherwise one is split at the mint and the change stays in the store. The chosen note is held pending until the outcome is known, so nothing else can spend it in the meantime. xCashu uses coin selection via `wallet.send(amount, proofs, { includeFees: true })` and restores change proofs to the token store. IETF Payment decodes the base64url charge request to extract the BOLT11 invoice, then pays it through the normal wallet stack and wraps the preimage in a JSON credential.
290
296
 
291
297
  ## Wallet Priority and Resolution
292
298
 
293
- Wallets are tried in order: NWC → Cashu → Human. The first wallet that succeeds is used.
299
+ Wallets are tried in order: NWC → Cashu → LNURLcash → Human. The first wallet that succeeds is used.
294
300
 
295
301
  | Wallet | Autonomy | Requirements | Speed |
296
302
  |--------|----------|-------------|-------|
297
303
  | NWC | Fully autonomous | `NWC_URI_FILE` pointing to a private 0600 bearer file | ~2-5 seconds |
298
304
  | Cashu | Fully autonomous | `CASHU_TOKENS` file with ecash tokens | ~3-8 seconds |
305
+ | LNURLcash | Fully autonomous | `LNURLCASH_NOTES` file with LUD-25 bearer notes | ~2-6 seconds |
299
306
  | Human | Requires human | None — always available as fallback | Minutes |
300
307
 
301
308
  The agent can force a specific method per-call using the `paymentMethod` parameter.
package/llms.txt CHANGED
@@ -120,10 +120,11 @@ Three payer methods are tried in priority order:
120
120
 
121
121
  The agent can override the payment method per-call, or you can configure only the methods you want available.
122
122
 
123
- `l402-fetch` handles four HTTP 402 challenge variants transparently:
123
+ `l402-fetch` handles five HTTP 402 challenge variants transparently:
124
124
 
125
125
  - **L402** (`WWW-Authenticate: L402`) — the primary protocol; Lightning invoice paid via the wallet stack.
126
126
  - **IETF Payment** (`WWW-Authenticate: Payment`, `draft-ryan-httpauth-payment-01`) — alternative Lightning path; builds a base64url credential after payment.
127
+ - **LNURLcash** (`X-LNURLcash: lnurlcashreq1…`, LUD-25): autonomous bearer-note payment; hands over a note worth exactly the price, splitting one at the mint if needed. Tried first: no Lightning hop and no swap at the mint.
127
128
  - **xCashu** (`X-Cashu: creqA…`, NUT-18) — autonomous ecash payment; selects and sends the exact token amount without a Lightning round-trip.
128
129
  - **x402** (`X-Payment-Required: x402`) — on-chain EVM payment; surfaces an EIP-681 deeplink to the human (no autonomous settlement).
129
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "402-mcp",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "mcpName": "dev.forgesworn/402-mcp",
5
5
  "description": "L402 + x402 client MCP - AI agents discover, pay for, and consume any payment-gated API",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "./l402/*": "./build/l402/*.js",
15
15
  "./x402/*": "./build/x402/*.js",
16
16
  "./xcashu/*": "./build/xcashu/*.js",
17
+ "./xlnurlcash/*": "./build/xlnurlcash/*.js",
17
18
  "./ietf-payment/*": "./build/ietf-payment/*.js",
18
19
  "./store/*": "./build/store/*.js",
19
20
  "./fetch/*": "./build/fetch/*.js",
@@ -81,7 +82,10 @@
81
82
  "ai-agent",
82
83
  "autonomous-payments",
83
84
  "macaroon",
84
- "stablecoin"
85
+ "stablecoin",
86
+ "lnurlcash",
87
+ "lud-25",
88
+ "bearer-notes"
85
89
  ],
86
90
  "author": "forgesworn",
87
91
  "homepage": "https://github.com/forgesworn/402-mcp",