NWC-KIT

Nostr Wallet Connect for TypeScript

Give your app a wallet connection,
not the wallet itself.

If your app, agent or paid API needs to spend sats, nwc-kit gives it a constrained NIP‑47 connection to the user's existing Lightning wallet. The user keeps the wallet and its keys; your application gets a narrow, authenticated way to ask it to act.

Watch it reject a forged wallet npm install @forgesworn/nwc-kit

One runtime dependency  ·  Node 22+, browsers, Deno, Bun  ·  NIP‑47 / NIP‑44 v2  ·  MIT  ·  v0.1.7, API not frozen

YOUR APP ASKS TO PAY NWC-KIT the client ENCRYPTED RELAY untrusted RELAYED WALLET keys · funds SIGNED · THEN VERIFIED
Fig. 1. nwc-kit is the protocol layer, not the wallet. Your app gets an authenticated, NIP‑44-encrypted connection over a relay it is never asked to trust; the wallet keeps its keys and funds. Every reply is checked — author, request reference, recipient and method — before a single byte is decrypted.

Early release. Verified against the NIP‑44 protocol vectors, the adversarial wallet below, and a controlled mainnet acceptance through Rizful in the 402‑mcp payment path. That proves one wallet and one provider path, not every real-world implementation. Pin an exact version.

01

Why would you need it?

nwc-kit is the protocol layer in the bridge between an application and a wallet. It is useful when software needs to request a payment, but must not become the wallet, hold a seed, or pretend a relay is trustworthy.

Build a paid service
Let a user-authorised wallet pay for an API call, a tool, compute or content without putting wallet keys on your server. This is the shape used by the 402‑mcp payment bridge.
Give an agent spending power
An agent can discover capabilities and request a payment through a bounded connection. Approval, budgets and business policy stay with the application and the wallet, never inside this protocol client.
Keep the boundary small
You get NIP‑47 transport, NIP‑44 v2 encryption and authenticated responses. You do not get custody, invoice verification or settlement claims. That narrow boundary is the point.
02

What it refuses to trust

The relay
Subscription filters are performance hints, not authentication. Every response is shape-checked, signature-verified and matched to the live request before anything is decrypted. A valid wallet event that does not reference the active request is treated as a possible replay and ignored, rather than allowed to fail the request.
Legacy encryption
NIP‑04 is refused, not merely deprecated. The client requires the wallet to advertise nip44_v2 in a signed capability event before it will send anything at all, and it checks the wallet author, request reference, client recipient and result method before trusting a result.
Silence
Once publication begins, timeout, abort, close and publish failure are ambiguous outcomes. A relay can store an event without returning a usable acknowledgement. The library says so, the types say so, and the docs say so, because a blind retry can pay twice.

A timeout is not a refund. Reconcile against the original invoice before you retry anything — the wallet is authoritative for whether it attempted a payment, and the absence of an answer tells you nothing about what it did.

03

Watch it reject a forged wallet

This page runs the real library. In simulated mode a wallet lives in the page, signs its own Nostr events and encrypts to you with real NIP‑44 v2, so the client cannot tell it from a remote one. Change how that wallet behaves and watch the client accept or reject it.

Demo wallet balance 0 sats

Nothing leaves your browser in this mode. There is no relay and no network request.

Protocol tape Ready.

Press Connect. Every event below is really signed, really encrypted and really verified by the library you would install.

04

The whole API

Two exports. The draft extension is kept behind the second one so the core cannot come to depend on it.

import { NwcClient } from '@forgesworn/nwc-kit'
import { tryDecodeBolt11, verifyPreimage } from 'farrier-kit'

const client = new NwcClient(connectionUri)

try {
  const caps = await client.connect()
  if (!caps.methods.includes('pay_invoice')) {
    throw new Error('This connection cannot pay')
  }

  const decoded = tryDecodeBolt11(invoice)
  if (!decoded || decoded.amountMsats === null) {
    throw new Error('Refusing an amountless invoice')
  }

  const result = await client.payInvoice({ invoice })

  if (!verifyPreimage(result.preimage, decoded.paymentHashHex)) {
    throw new Error('Response does not settle this invoice')
  }
} finally {
  client.close()
}
connect()
Signed capability discovery. Nothing is sent until the wallet has advertised nip44_v2 in an event it signed itself.
payInvoice()
Returns a validated preimage. Validated means well-formed and authenticated — not that the payment settled. That check is yours.
makeInvoice()
Amount in milli-satoshis, like every other amount, balance and fee field on this page. Convert sats explicitly at the application boundary and reject ambiguous amounts before making a request.
lookupInvoice()
By payment hash or by invoice. The only honest way to answer “did that actually settle?” after an ambiguous outcome.
getBalance(), getInfo()
Milli-satoshis, and wallet metadata.
close()
Cancels outstanding work and zeroises key material.
inspectNwcConnection()
Reads a connection URI — relays, wallet pubkey, budget hints — without exposing the secret.

Transaction history is draft extension 05 and lives at @forgesworn/nwc-kit/extensions/05. It always sends an explicit page limit, capped at 20, because a wallet asked for no particular page size applies a larger default of its own — Alby Hub's is 50.

05

A worked recipe: LNURLcash

LNURLcash turns a Lightning payment preimage into the secret for a bearer note. nwc-kit covers the wallet leg in both directions, so a browser app can mint and melt notes through the user's existing NWC connection without growing a wallet backend of its own.

01 · mint a note

Pay the mint's invoice

Call payInvoice(), verify the returned preimage against the invoice payment hash, then claim and rotate the note. The preimage is the bearer secret, so accepting the wrong one means accepting a worthless note.

02 · melt a note

Invoice the mint back

Call makeInvoice(), give that invoice to the mint, then poll lookupInvoice() until your wallet reports it settled. An accepted melt request is not settlement proof.

The boundary stays put. nwc-kit does not implement LNURLcash, store bearer notes or verify invoices. The worked recipe uses farrier-kit for the money checks and has been run against a live experimental mint. LNURLcash is still a draft, so keep the connection and the note value small while testing.

06

What it refuses to do

Not in scope, deliberately

  • Wallet or node implementation
  • Custody, balances, subwallet management
  • BOLT-11, LNURL, L402, Cashu, WebLN, fiat
  • Amount policy and user approval
  • Settlement verification
  • Legacy NIP-04 interoperability

What you still owe your users

The wallet service is authoritative for whether it attempted a payment, but its response is not settlement proof. Verify the invoice before you pay it and verify the returned preimage against the invoice payment hash afterwards. farrier-kit does both.

Wallet-enforced permissions and budgets are defence in depth, not a replacement for your own approval and spend policy. Passing tests are not evidence that money settled.

§

The rest of the rail

Each of these does one job and refuses the next one. Together they are a payment rail an application can assemble itself, without any single piece asking to be trusted with everything.