npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

beignet

v0.27.0

Published

A self-custodial, JS Bitcoin wallet + Lightning Network management library.

Readme

Beignet

A self-custodial Bitcoin wallet library for JavaScript/TypeScript with a full Lightning Network implementation. Beignet implements the Lightning protocol and channel state machine in TypeScript rather than wrapping LND, CLN or LDK: it speaks BOLT 8 over a real TCP socket and runs its own BOLT 2 state machine.

Two layers, one mnemonic:

  • On-chain wallet: HD keys, address generation, UTXO tracking, transaction building, PSBT/hardware signing, multisig, watch-only, Electrum connectivity.
  • Lightning: channel lifecycle, onion-routed payments, BOLT 11 invoices, BOLT 12 offers, gossip and pathfinding, anchors, splicing, taproot channels, watchtower client. Interop-tested against LND, Core Lightning and Eclair on regtest.

Requires Node.js 18+. MIT licensed.

Jump to: Install · Examples · On-chain wallet · Lightning · Daemon & CLI · Protocol layer · Tests · Status & limitations

Install

npm install beignet     # or: yarn add beignet

Smallest thing that works. net and tls are injected so the same code runs on Node and React Native:

import net from 'net';
import tls from 'tls';
import { Wallet, generateMnemonic } from 'beignet';

const result = await Wallet.create({
  mnemonic: generateMnemonic(),
  electrumOptions: { net, tls }
});
if (result.isErr()) throw result.error;
const wallet = result.value;

console.log(await wallet.getAddress());
console.log(wallet.getBalance());

From here: the on-chain wallet for sending, PSBTs, multisig and watch-only, or Lightning for channels and payments.

Try the examples

The fastest way to understand the whole system is to run the two REPL examples against a live wallet and a live node. Both are checked-in TypeScript you can read and edit.

git clone [email protected]:coreyphillips/beignet.git && cd beignet
npm install

1. On-chain wallet REPL

npm run example

Creates a mainnet wallet (a fresh mnemonic unless you pass one), syncs it against a public Electrum server, prints the balance and a receive address, then drops you at a > prompt with the wallet bound to wallet. Type help() for the command list.

> wallet.getBalance()
> await wallet.getAddress()
> await wallet.refreshWallet()
> await wallet.send({ address: 'bc1q...', amount: 10000, satsPerByte: 2 })

State persists as JSON under example/walletData/. Pass a mnemonic as the first argument to reuse a wallet: npm run example -- "abandon abandon ... about".

2. Lightning node REPL

npm run example:lightning

Boots a real Lightning node (BeignetNode) with an auto-created wallet, storage and funding provider, waits for it to become operational, prints info/balance/health, and drops you at a beignet> prompt with the node bound to node. Type help() for the command list. Top-level await works.

beignet> await node.getNewAddress()          // fund this on-chain, then:
beignet> await node.connectAndOpenChannel(pubkey, host, port, 200000)
beignet> node.createInvoice(1000, 'coffee').bolt11
beignet> await node.payInvoice('lnbc...')
beignet> node.getLiquiditySnapshot()

The flags you will actually reach for (everything after --):

| Flag | Effect | |------|--------| | mainnet | testnet | regtest | Network, as a bare positional arg (default mainnet) | | <12 or 24 words> | Reuse a mnemonic, as bare positional args (default generates one) | | --electrum-host <h> --electrum-port <p> | Point at your own Electrum server | | --alias <name> | Node alias in node_announcement |

# named regtest node against a local Electrum server
npm run example:lightning -- regtest --electrum-host 127.0.0.1 --electrum-port 60001 --alias mynode

Tor, full-graph gossip, the low-level LightningNode variant and the non-interactive payment-API walkthrough have flags too: see the flag reference.

Node state lives in a SQLite DB under ~/.beignet/data/<hash-of-mnemonic>/ (the --low-level example uses example/lightningData/node.db).

Both examples run straight off the TypeScript sources through ts-node, so no build step is needed. Use npm run build when you want the compiled dist/.

→ example/REPL_TESTING.md is a copy-pasteable walkthrough of the whole lifecycle in the REPL: funding, peers, channels, invoices, payments, keysend, offers, splicing, closing, backup.

Which entry point?

| Import | Contains | Use when | |--------|----------|----------| | beignet | Wallet, generateMnemonic, types | You want the on-chain wallet | | beignet/cli | BeignetNode, startDaemon, error helpers | You want Lightning. Sats-denominated, string IDs, structured errors | | beignet/lightning | Namespaced protocol modules (node, channel, onion, ...) | You need the raw BOLT layer: bigint msat, Buffer IDs, wire messages |

On-chain wallet

import net from 'net';
import tls from 'tls';
import { Wallet, generateMnemonic } from 'beignet';

const res = await Wallet.create({
  mnemonic: generateMnemonic(),
  electrumOptions: { net, tls } // required: inject the socket implementations
});
if (res.isErr()) throw res.error;
const wallet = res.value;

const address = await wallet.getAddress();
const balance = wallet.getBalance();

await wallet.send({ address: 'bc1q...', amount: 50_000, satsPerByte: 2 });
await wallet.sendMany({ txs: [{ address: 'bc1q...', amount: 1000 }] });
await wallet.refreshWallet();

const utxos = wallet.listUtxos();
const history = await wallet.getAddressHistory('bc1q...');

Every fallible call returns a Result<T>: check isErr() before reading .value. Amounts are always satoshis.

Options worth knowing on Wallet.create: network (EAvailableNetworks.mainnet | testnet | regtest | signet), addressType (p2wpkh default, p2sh-p2wpkh, p2pkh, p2tr), passphrase, account, storage, logger, coinSelectPreference, feeEstimationSource, gapLimitOptions.

import { EAvailableNetworks, EProtocol, Wallet } from 'beignet';

const res = await Wallet.create({
  mnemonic,
  network: EAvailableNetworks.mainnet,
  feeEstimationSource: 'electrum', // 'electrum' | 'http' | 'auto' (default)
  electrumOptions: {
    net,
    tls,
    servers: [
      { host: 'bitcoin.lu.ke', ssl: 50002, tcp: 50001, protocol: EProtocol.ssl },
      { host: 'mempool.space', ssl: 60602, tcp: 60601, protocol: EProtocol.ssl }
    ]
  }
});
  • Failover: with multiple servers the wallet rotates through them in order on connect/reconnect failure, then through hardcoded fallback peers for the network, with a per-server cooldown so dead servers are not hammered. Inspect wallet.electrum.currentServer and wallet.electrum.rotationCount.
  • Certificate verification: by default a TLS Electrum connection is encrypted but the server certificate is not checked, because the client library dials with rejectUnauthorized: false. An on-path attacker can therefore stand in for the server. On Node, pass tls: withTlsVerification(tls) to accept only certificates that chain to a trusted CA and match the host, and add { fingerprints: ['AB:CD:…'] } to also accept self-signed servers by SHA-256 fingerprint (openssl x509 -noout -fingerprint -sha256). The CLI/daemon and React Native do not verify yet.
  • Fee source: 'electrum' queries only the connected server via blockchain.estimatefee, so fee lookups never leak to mempool.space/blocktank over clearnet. 'auto' prefers Electrum and falls back to HTTP. All remote rates are clamped to 5000 sat/vB.
  • Networks: mainnet, testnet, regtest and signet work end to end (wallet, Electrum, CLI/daemon --network signet, Lightning chain hash and tbs invoice prefix). Signet shares testnet address formats and coin type 1.
  • BIP21: encodeBip21({ address, amountSats?, label?, message? }) builds a bitcoin: URI.

Built from an account-level extended public key instead of a mnemonic. The key is assumed to sit at m/purpose'/coin'/account' (e.g. m/84'/0'/0'), so addresses derive as xpub/0/i and xpub/1/i. SLIP-132 version bytes are normalized: zpub/vpub implies p2wpkh, ypub/upub implies p2sh-p2wpkh, a plain xpub/tpub uses addressType (default p2wpkh). One account xpub yields exactly one address type, so a watch-only wallet monitors only that type.

const res = await Wallet.createWatchOnly({
  xpub: 'zpub6r...',
  network: EAvailableNetworks.mainnet,
  electrumOptions: { net, tls }
});
if (res.isErr()) return;
const watchOnly = res.value;

await watchOnly.getAddress(); // works
watchOnly.getBalance();       // works

const send = await watchOnly.send({ address: 'bc1q...', amount: 1000 });
// send.isErr() === true, message: 'watch-only wallet cannot sign'

The full read-only surface works: address generation, gap-limit scanning, Electrum refresh, balances, history, UTXOs, fee estimates, address subscriptions. Anything needing private keys (send/sendMax/sendMany/sweepPrivateKey/getPrivateKey) fails with the typed WatchOnlySigningError (code: 'WATCH_ONLY_CANNOT_SIGN'). Library-only for now: the HTTP daemon always runs with a mnemonic.

buildPsbt runs the normal setup (coin selection, change, fee) but stops before signing, returning a base64 PSBT populated with what a hardware signer needs: witnessUtxo (or nonWitnessUtxo for legacy p2pkh), redeemScript for p2sh-p2wpkh, tapInternalKey plus tapBip32Derivation for p2tr, and bip32Derivation on every wallet input. The change output carries the same derivation fields, so the signer shows it as change rather than as a second recipient. Works on full and watch-only wallets.

// 1. Build (never touches private keys)
const build = await wallet.buildPsbt({ address: 'bc1q...', amount: 50_000, satsPerByte: 4 });
if (build.isErr()) return;
const { psbtBase64, fee, vsizeEstimate } = build.value;

// 2. Sign externally (hardware wallet, HWI, another machine)
const signedBase64 = await myHardwareWallet.signPsbt(psbtBase64);

// 3. Import: checks the inputs and outputs are the ones built, validates a
//    signature on EVERY input, finalizes, does NOT broadcast
const imported = wallet.importSignedPsbt(signedBase64);
if (imported.isErr()) return; // changed outputs, missing/invalid signatures are rejected loudly
const { txHex, txid } = imported.value;

// 4. Broadcast when ready
await wallet.broadcastTransaction(txHex);

// Multi-party: merge partially signed copies of the same PSBT
const combined = wallet.combinePsbts([copyA, copyB]);

importSignedPsbt finalizes only a PSBT that spends the same inputs to the same outputs as one this wallet instance built (it remembers its 50 most recent builds, in memory). To import a PSBT built elsewhere, or after a restart, pass the unsigned PSBT as the second argument: wallet.importSignedPsbt(signedBase64, psbtBase64). Inputs the signer already finalized are refused, since their signatures cannot be checked.

For watch-only wallets the true master fingerprint is unknowable from an account xpub, so the xpub's parent fingerprint is used: signers should locate keys by derivation path.

Also on the daemon (POST /psbt/build, /psbt/import-signed, /psbt/combine) and the CLI (beignet psbt build|import-signed|combine). A restart forgets the daemon's builds, so the import there takes the unsigned PSBT too: unsignedPsbtBase64 on the route, a second argument to beignet psbt import-signed.

Wallet.createMultisig creates a descriptor-based sorted-multisig wallet, wsh(sortedmulti(threshold, key1, key2, ...)): the interoperable standard used by Bitcoin Core, Sparrow and Specter. Derivation follows BIP 48 script type 2 (m/48'/coin'/account'/2', receive /0/*, change /1/*) and keys are BIP 67 ordered at every index, so any wallet built from the same account xpubs produces identical addresses regardless of cosigner order.

Cosigners are account-level extended public keys (xpub/tpub, or SLIP-132 Zpub/Vpub, normalized automatically). With a mnemonic, this wallet IS one of the cosigners: its BIP 48 account xpub is derived and included automatically (pass ourXpub to assert it; a mismatch is rejected). Omit the mnemonic for a watch-only coordinator.

Spending is PSBT-only. send/sendMany/sendMax fail with MultisigSpendError (code: 'MULTISIG_REQUIRES_PSBT').

// 1. Each cosigner builds the same quorum from the others' BIP 48 account xpubs.
const a = await Wallet.createMultisig({
  threshold: 2,
  mnemonic: mnemonicA,       // we are one cosigner; our xpub is added automatically
  cosigners: [xpubB, xpubC],
  network: EAvailableNetworks.mainnet,
  electrumOptions: { net, tls }
});
const b = await Wallet.createMultisig({ threshold: 2, mnemonic: mnemonicB, cosigners: [xpubA, xpubC], /* ... */ });

// An optional watch-only coordinator holds no keys at all.
const c = await Wallet.createMultisig({ threshold: 2, cosigners: [xpubA, xpubB, xpubC], /* ... */ });

if (a.isErr() || b.isErr() || c.isErr()) return;
const [walletA, walletB, coordinator] = [a.value, b.value, c.value];

// 2. Fund it: every instance derives the same addresses.
const deposit = await walletA.getAddress();

// 3. Build the unsigned PSBT (any instance, coordinator included).
const built = await walletA.buildPsbt({ address: 'bc1q...', amount: 50_000, satsPerByte: 4 });
if (built.isErr()) return;
const unsigned = built.value.psbtBase64;

// 4. Each cosigner signs their own copy (nothing finalizes below threshold).
const signedA = walletA.signPsbtWithOurKey(unsigned);
const signedB = walletB.signPsbtWithOurKey(unsigned);
if (signedA.isErr() || signedB.isErr()) return;

// 5. Combine, finalize at threshold, broadcast. The coordinator did not build
//    the PSBT, so it checks the combined one against the unsigned original.
const combined = coordinator.combinePsbts([signedA.value, signedB.value]);
if (combined.isErr()) return;
const finalized = coordinator.importSignedPsbt(combined.value, unsigned); // 2-of-3 met
if (finalized.isErr()) return;
await coordinator.broadcastTransaction(finalized.value.txHex);

// Below threshold it fails loudly:
// 'Input 0 is below the multisig threshold: have 1 signature(s), need 2.'

// Interop: import into Bitcoin Core / Sparrow / Specter.
coordinator.exportDescriptors();
// wsh(sortedmulti(2,[fp/48h/0h/0h/2h]xpub.../0/*,[fp]xpub.../0/*,...))#checksum

buildPsbt attaches the witnessScript and one bip32Derivation per cosigner to every input. importSignedPsbt counts VALID partial signatures per input against the witnessScript threshold and refuses to finalize below it. Library-only for now: the daemon wallet stays single-sig.

The wallet persists through the host-injected TStorage interface (storage: { getData, setData }), and values are handed over as-is, so by default they are stored in plaintext. Persisted data is addresses, indexes, UTXOs, transactions, balance and fee estimates: no private keys and no mnemonic are ever written, so exposure is a privacy concern (full wallet history), not fund loss. The staged send (transaction) is written without signing keys, so a key pair handed to sweepPrivateKey or addExternalInputs never reaches storage, and send, sendMany, sendMax, buildPsbt and sweepPrivateKey reset the staged send when they return, so a restart never replays an earlier call's recipients.

Wrap any TStorage with createEncryptedStorage to encrypt at rest with AES-256-GCM under an HKDF-derived key from the seed. Pre-existing plaintext values pass through unchanged and migrate lazily as they are rewritten.

import * as bip39 from 'bip39';
import { createConsoleLogger, createEncryptedStorage, Wallet } from 'beignet';

const seed = bip39.mnemonicToSeedSync(mnemonic);
const wallet = await Wallet.create({
  mnemonic,
  storage: createEncryptedStorage({ getData, setData }, seed),
  logger: createConsoleLogger('warn'), // only warn + error reach the console
  electrumOptions: { net, tls }
});

Diagnostics flow through a small injectable ILogger (debug/info/warn/error, each (message, meta?)), with filtering debug < info < warn < error plus 'silent'. This is separate from the Lightning node's persisted structured action log (getActionLog).

  • Wallet.create({ logger }) defaults to createConsoleLogger('info'), preserving historical console output. disableMessages is independent: it only gates onMessage callbacks.
  • LightningNode defaults to noopLogger (silent). Every action-log entry is also mirrored to logger.debug('category:action', data).
  • BeignetNode.create({ logger, logLevel }) forwards passing entries to the logger (in addition to the 'log' event) and injects it into the underlying Wallet and LightningNode.
  • Daemon: beignet start --log-level <debug|info|warn|error|silent> (or BEIGNET_LOG_LEVEL, or logLevel in ~/.beignet/config.json) prints to stderr. Unset keeps the daemon silent; stdout stays reserved for command output.

Lightning

Beignet is under active development. Evaluate it on regtest, signet, or with small amounts you can afford to lose. Read Status & limitations before putting meaningful mainnet funds behind it: this is a self-custodial Lightning implementation, and channel funds are only as safe as the node watching them.

BeignetNode from beignet/cli is the recommended API: it wraps the protocol layer with satoshi amounts, string channel IDs and structured error codes.

import { BeignetNode, isRetryableError } from 'beignet/cli';

// Creates the wallet, storage and funding provider for you
const node = await BeignetNode.create({
  mnemonic: 'abandon abandon ... about',
  network: 'regtest',
  electrumHost: '127.0.0.1',
  electrumPort: 60001
});

node.getInfo();    // { nodeId, network, alias, ... }
node.getHealth();  // { status: 'ready', peers, channels, ... }
node.isReady();    // true once the node has active channels

const inv = node.createInvoice(1000, 'coffee');
console.log(inv.bolt11);

try {
  const payment = await node.payInvoice('lnbcrt10n1...');
  console.log(payment.status); // 'COMPLETED'
} catch (err) {
  if (isRetryableError(err)) {
    // transient: no route, timeout. Safe to retry
  } else {
    // permanent: invalid invoice, expired. Do not retry
  }
}

node.listChannels();
node.listPayments();
node.listInvoices();

await node.destroy();

Events: node:ready, channel:ready, channel:closed, channel:resolved, peer:connect, peer:disconnect, peer:error, payment:sent, payment:received, node:error, log.

Useful variants: payInvoiceSafe (never throws), payInvoiceWithRetry({ maxRetries, backoffMs, maxFeeSats }), sendPaymentAsync (returns the hash immediately), connectAndOpenChannel, openChannelAndWait, sendKeysend, createOffer/payOffer, spliceIn/spliceOut, backup, gracefulShutdown.

→ docs/AI_AGENT_GUIDE.md covers deployment in depth: channel strategy, liquidity management, monitoring and Prometheus metrics, pre-flight validation, safety rails, retry/backoff patterns, idempotency keys, spend limits, drain mode, backup and recovery, mainnet checklist.

Decision-support APIs

Built-in advisors, not usually found in a Lightning library:

// Channel balance analysis with actionable recommendations
const liquidity = node.getLiquiditySnapshot();
console.log('Outbound:', liquidity.outboundLiquidityPct + '%');
for (const rec of liquidity.recommendations) {
  console.log(`[${rec.priority}] ${rec.type}: ${rec.reason}`);
}

node.getChannelSuggestions(3);  // graph-based peer suggestions for opens
node.getFeeSnapshot();          // on-chain fee trend: OPEN_NOW / WAIT / NEUTRAL
node.estimatePayment(bolt11);   // success probability + estimated fee, pre-send
node.getMainnetReadiness();     // 12-check weighted readiness report

The advisor can act, not just recommend. Both features are off by default.

// One-shot circular rebalance: self-payment out over `from` and back in over `to`.
// Aborts WITHOUT paying if the route fee exceeds maxFeeSats.
await node.rebalanceChannel(fromChannelId, toChannelId, 50_000, /* maxFeeSats */ 50);

node.getAdvisorRecommendations();        // read-only: analyze() + rebalancePlan[]
await node.executeRebalances(/* budgetSatsPerDay */ 500);

Automatic modes, opt-in via BeignetNodeOptions / INodeConfig:

const node = await BeignetNode.create({
  mnemonic,
  // Periodically executes the rebalance plan. Routing fees spent on rebalances
  // are capped per UTC day and the running spend is persisted, so restarts
  // never overspend the same day. Resets at midnight UTC.
  autoRebalance: { enabled: true, budgetSatsPerDay: 500, minImbalancePct: 20 },
  // Every intervalMs (default 6h) nudges each channel's proportional fee:
  // +25% when outbound is depleted (<20% local) but still forwarding, -25% when
  // the channel saw no forwards in the window, clamped to [floorPpm, ceilPpm].
  // One adjustment per channel per interval.
  autoTuneFees: { enabled: true, floorPpm: 1, ceilPpm: 5_000 }
});

Daemon: POST /rebalance, GET /advisor/recommendations, POST /advisor/execute-rebalances. CLI: beignet rebalance <from> <to> <sats> --max-fee <sats>, beignet advisor recommendations, beignet advisor execute-rebalances [--budget <sats>].

Penalty enforcement normally needs this node's chain monitor to be online: if a counterparty broadcasts a revoked commitment while you are offline, nobody sweeps the breach. The watchtower client closes that gap. At every revocation it builds an encrypted justice kit (the revoked commitment's breach hint plus a pre-signed to_local penalty) and ships it to remote towers over BOLT 8. When a tower later sees the breach on chain it decrypts the kit and broadcasts the penalty for you.

const node = await BeignetNode.create({
  mnemonic,
  watchtowers: ['[email protected]:9911'] // off when empty
});
  • Altruist only. Sessions use reward = 0. There is no server mode: beignet is a tower client, not a tower.
  • LND-tower compatible. Implements LND's wtwire protocol (Init/CreateSession/StateUpdate/DeleteSession, message types 600-607) and the version-0 justice blob (XChaCha20-Poly1305, breach hint SHA256(txid)[:16], key SHA256(txid‖txid)), so it works with existing public LND altruist towers.
  • Legacy + anchor channels. The to_local revocation penalty (the fund-critical punishment) is packed for both. Taproot channels are not yet backed up.
  • Durable. Per-tower session state and the un-acked backlog are persisted (encrypted at rest) and drained with exponential backoff on reconnect. An un-acked update is never dropped silently.

Daemon: GET /watchtowers, POST /watchtower/add, DELETE /watchtower/remove. CLI: beignet watchtower list|add <pubkey@host:port>|remove <uri>, daemon flag --watchtower (repeatable) or BEIGNET_WATCHTOWERS.

HTTP daemon & CLI

The same node runs as an HTTP/SSE daemon for language-agnostic integrations, driven by a JSON CLI.

# 1. Generate a mnemonic, an API token and ~/.beignet/config.json
npx beignet init --network regtest
# {"ok":true,"result":{"message":"Initialized","mnemonic":"...","network":"regtest",
#   "apiToken":"3f9c...64 hex...","note":"apiToken was generated and saved to config.json; ..."}}

# 2. Start the daemon (add --daemon to background it). It reads the token from
#    config.json; --api-token or BEIGNET_API_TOKEN override it.
BEIGNET_ELECTRUM_HOST=127.0.0.1 BEIGNET_ELECTRUM_PORT=60001 BEIGNET_ELECTRUM_TLS=false \
  npx beignet start --network regtest

# 3. Drive it with the CLI (thin HTTP client, JSON out; it sends the token itself)
npx beignet info --pretty
npx beignet address
npx beignet channel connect-and-open <pubkey> <host> <port> 200000
npx beignet invoice create 1000 "coffee"
npx beignet invoice pay <bolt11>

Electrum and most other settings come from ~/.beignet/config.json or the environment (BEIGNET_MNEMONIC, BEIGNET_ELECTRUM_HOST, BEIGNET_ELECTRUM_PORT, BEIGNET_NETWORK, ...). Run npx beignet help for the full command and flag list.

config.json holds the mnemonic, so everything under ~/.beignet is created owner-only (0700 directories, 0600 files: config, pid file, database and sidecars, backups, SCB exports), the CLI runs init, start, backup and restore under umask 077, and a config file an earlier release left readable is tightened the next time it is read, with a notice on stderr. Details in src/cli/README.md.

Or over HTTP directly, with the token init printed (or apiToken from ~/.beignet/config.json):

TOKEN=3f9c...   # the apiToken from beignet init
curl -X POST http://localhost:2112/invoice/create -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"amountSats": 1000, "description": "coffee"}'

curl -X POST http://localhost:2112/invoice/pay -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"bolt11": "lnbcrt10n1..."}'

curl -N http://localhost:2112/events -H "Authorization: Bearer $TOKEN"  # SSE stream
curl http://localhost:2112/ready                                       # load-balancer probe
  • Responses are { "ok": true, "result": {...} } or { "ok": false, "error": { "code": "...", "message": "..." } }.
  • Full spec at GET /openapi.json.
  • Authentication is on for every install beignet init creates (releases after 0.22.0): init mints a random apiToken and saves it in config.json (run init again on an older config to add one). Auth is off only for a config with neither apiToken nor apiKeys (named keys with readonly/invoice/admin scopes); beignet start warns on stderr in that case. GET /health, /ready and /openapi.json are auth-exempt; /metrics only with metricsPublic; everything else requires the bearer token. The daemon binds 127.0.0.1 by default.
  • While auth is off, three browser guards keep a web page from driving the loopback daemon (issue #1005): a request body must be Content-Type: application/json (else 415 UNSUPPORTED_MEDIA_TYPE), an Origin other than the configured cors origin or a Sec-Fetch-Site: cross-site request is refused (403 CROSS_SITE_REQUEST_REFUSED), and the Host header must be the loopback name the daemon is bound on (else 421 HOST_NOT_ALLOWED, which also defeats DNS rebinding). They apply to every route but OPTIONS; plain clients (curl, the CLI, the SDKs) send none of those headers and are unaffected, and with a token configured the guards do not run at all.
  • Embed it instead of shelling out: import { startDaemon } from 'beignet/cli'.

FFOR offline receive

Fast-Forward Offline Receive (spec: github.com/coreyphillips/ffor, Variant D) lets a wallet receive while offline through a settlement peer that holds a pre-signed voucher book. The daemon exposes the receiver's lifecycle under /ffor/* (/ffor/epoch/start, /ffor/invoice, /ffor/epoch/close, /ffor/preimage, /ffor/witness/provision, /ffor/issuer/offer, /ffor/issuer/provision, /ffor/sync, /ffor/recover, /ffor/enforce, /ffor/epochs, /ffor/epoch, /ffor/witness/close, /ffor/issuer/issued) and three roles a node can run for others, each an explicit opt-in switched on with an exact true:

| Env | Role | |---|---| | BEIGNET_FFOR_SETTLE | Answer ff_init as a settlement peer. BEIGNET_FFOR_MAX_BUDGET_MSAT, BEIGNET_FFOR_MAX_EPOCH_BLOCKS, BEIGNET_FFOR_FEE_BASE_MSAT and BEIGNET_FFOR_FEE_PPM bound what it accepts. Off by default: an epoch locks the whole budget of this node's liquidity. GET /ffor/settlements lists them. | | BEIGNET_FFOR_WITNESS | Store a receiver-encrypted record of every delegated preimage this node relays before propagating the fulfil (a receipt witness). BEIGNET_FFOR_WITNESS_MAX_MAILBOXES and BEIGNET_FFOR_WITNESS_MAX_BYTES cap it. GET /ffor/witness/status. | | BEIGNET_FFOR_ISSUER | Answer BOLT 12 invoice requests for offers a receiver delegated to this node, one fixed-amount slot per invoice. Needs the witness. GET /ffor/issuer/status. |

The SSE stream carries ffor:state, ffor:settled, ffor:slot-resolved, ffor:delegated-failed, ffor:enforce and the witness and issuer events.

The experimental concurrent profile keeps ordinary payments available within the channel's remaining capacity. LightningNode.fforSync() fetches paid slot receipts and redeems them while other vouchers remain live. rescueFforEpoch() syncs a connected concurrent book without retiring it. Feature bits 562/563 are advertised by default when their dependencies are enabled. See concurrent receive and reserved retirement for the version 1 and 2 contracts and storage requirements. The daemon enables advertisement with BEIGNET_FFOR_CONCURRENT=true and new concurrent settlement with BEIGNET_FFOR_SETTLE_CONCURRENT=true; both default to true. Set either to false to disable it. Settlement still requires its separate role to be enabled. The automatic coordinator can reuse a funded home channel when the peers negotiate concurrent settlement. See the automatic receive API for sync, retirement and reserved-capacity fields.

Swaps (Lightning to on-chain, and on-chain to Lightning)

A beignet node can serve swaps to any Lightning peer in both directions. Reverse (issue #737): the peer pays a hold invoice, this node funds a P2WSH contract the peer claims on chain with its preimage, and the claim settles the hold. Submarine (issue #743): the peer locks coins in a P2WSH contract, this node pays the peer's own invoice under an absolute HTLC expiry ceiling, and the preimage that payment reveals claims the coins. Each direction is an explicit opt-in switched on with an exact true, because it puts this node's own funds at risk for peers:

| Env | Role | |---|---| | BEIGNET_SWAPS | Serve reverse swaps. BEIGNET_SWAP_FLAT_FEE_SAT and BEIGNET_SWAP_FEE_PPM price them; BEIGNET_SWAP_MIN_SAT, BEIGNET_SWAP_MAX_SAT, BEIGNET_SWAP_MAX_EXPOSURE_SAT and BEIGNET_SWAP_MAX_CONCURRENT cap what is at risk; BEIGNET_SWAP_REFUND_DELTA_BLOCKS, BEIGNET_SWAP_FUNDING_CONFS and BEIGNET_SWAP_RESOLUTION_CONFS set the timing. GET /swaps/status, GET /swaps, POST /swaps/cancel. | | BEIGNET_SWAP_SUBMARINE | With BEIGNET_SWAPS, also serve submarine swaps (on-chain to Lightning): a peer locks coins in a contract, this node pays the peer's invoice under an absolute HTLC expiry ceiling and claims the coins with the preimage. BEIGNET_SWAP_CLAIM_SAFETY_BLOCKS, BEIGNET_SWAP_PAYMENT_MAX_FEE_PPM, BEIGNET_SWAP_CLAIM_BUMP_INTERVAL_BLOCKS and BEIGNET_SWAP_SUBMARINE_REFUND_DELTA_BLOCKS set the direction's margins; the fee and exposure caps above apply to both. |

The reverse provider funds only against the complete committed MPP set of the hold invoice, settles the hold the moment a claim reveals the preimage (mempool included), and cancels the hold only after its own refund has confirmed to policy depth; never because the refund height passed. The submarine provider pays only once the peer's funding has confirmed to policy depth and been re-verified unspent immediately before the dispatch, binds every HTLC of the payment to refundHeight minus its claim margins, judges the payment by the node's own HTLC view (never by a wall clock or a failed record while an HTLC is out), and persists its claim before broadcasting it. The SSE stream carries swap:created through swap:settled, swap:refunded and swap:exposed for the reverse direction and swap:funding-seen, swap:paying, swap:preimage, swap:claim-broadcast, swap:claim-confirmed and swap:payment-failed for the submarine one.

Protocol layer (advanced)

Use this only if you need the BOLT layer directly: bigint msat, Buffer IDs, raw wire messages. beignet/lightning exports namespaces, not flat symbols.

import net from 'net';
import tls from 'tls';
import { Wallet, generateMnemonic } from 'beignet';
import { invoice, node as ln, wallet as lnWallet } from 'beignet/lightning';

const mnemonic = generateMnemonic();

// 1. On-chain wallet (the same mnemonic funds both layers)
const res = await Wallet.create({ mnemonic, electrumOptions: { net, tls } });
if (res.isErr()) throw res.error;

// 2. Lightning node with auto-funding from the wallet
const node = ln.LightningNode.fromMnemonic(mnemonic, {
  network: invoice.Network.REGTEST,
  enableNetworking: true,
  fundingProvider: new lnWallet.WalletFundingProvider(res.value)
});

// 3. Connect and open: fully automatic with a funding provider
await node.connectPeer('03...pubkey', '127.0.0.1', 9735);
node.openChannel('03...pubkey', 100_000n);

// 4. Invoice and payment
node.createInvoice({ amountMsat: 50_000n, description: 'coffee' });
node.sendPayment(invoiceString);

// 5. Events. Channel-scoped events carry an object, not a bare id
node.on('channel:ready', ({ channelId }) => console.log(channelId.toString('hex')));
node.on('payment:received', (p) => console.log(p.amountMsat, 'msat'));
node.on('node:error', (err) => console.error(`[${err.code}]`, err.message));

Without a fundingProvider, build the funding transaction yourself and call node.createFunding(channel, fundingTxid, outputIndex, signature) after openChannel.

LightningNode              High-level API (EventEmitter)
  ├── ChannelManager       Multiplexes messages to Channel instances
  │     └── Channel        BOLT 2 state machine (returns ChannelAction[])
  ├── PeerManager          TCP connections + Noise_XK encrypted transport
  │     └── Peer           Per-connection BOLT 8 handshake + message framing
  ├── NetworkGraph         BOLT 7 gossip topology + Dijkstra pathfinding
  ├── InvoiceManager       BOLT 11 encode/decode/sign
  ├── ChainMonitor         BOLT 5 force-close detection + sweep
  └── FundingProvider?     Auto-builds + broadcasts funding txs (via Wallet)

Key design principle: Channel is fully transport-agnostic. Every method returns a ChannelAction[] (send message, broadcast tx, watch output, ...) that ChannelManager maps to real transport or chain operations, which makes the state machine testable without network I/O.

→ src/lightning/README.md documents the protocol layer in detail: data flow, events reference, typed payment errors, channel lifecycle, zero-conf, anchors, dual funding, splicing, offers, onion messages, forwarding, chain monitoring.

| BOLT | Specification | Implemented | |------|--------------|-------------| | 1 | Base Protocol | Peer messaging, init, error, ping/pong, feature negotiation, peer storage | | 2 | Channel Management | Full state machine: open, fund, normal operation, shutdown, close, reestablish; v2 dual-funded opens (interactive-tx), splicing, quiescence | | 3 | Transactions | Commitment txs, HTLC scripts, funding scripts, anchor outputs, fee calculation; simple taproot channels (MuSig2 funding, Schnorr HTLC sigs) | | 4 | Onion Routing | Sphinx encryption, TLV hop payloads, payment_secret, failure codes, route blinding, onion messages | | 5 | On-Chain | Force-close detection, HTLC sweep, output resolution, chain monitoring, wallet-funded anchor fee bumping (commitment CPFP + zero-fee HTLC fee-attach) | | 7 | Gossip | Channel/node announcements, network graph, Dijkstra routing, gossip sync, Rapid Gossip Sync | | 8 | Transport | Noise_XK handshake, encrypted transport, key rotation | | 9 | Features | DATA_LOSS_PROTECT, STATIC_REMOTE_KEY, PAYMENT_SECRET, TLV_ONION, BASIC_MPP, CHANNEL_TYPE, GOSSIP_QUERIES, ANCHORS_ZERO_FEE_HTLC_TX (default), ROUTE_BLINDING, ONION_MESSAGES, QUIESCE, SCID_ALIAS, ZERO_CONF, KEYSEND, OPTION_TAPROOT, OPTION_WILL_FUND | | 10 | DNS Bootstrap | Seed resolution for discovering initial peers | | 11 | Invoices | Encode, decode, sign, verify, amount formatting, hold invoices | | 12 | Offers | Offer encode/decode, invoice_request/invoice over onion messages, receive-side settlement, async payment offers | | bLIP-51 | Liquidity Ads | lease_rates/request_funds/will_fund negotiation, lease fee accounting, CLTV-locked lessor to_local, advisor lease quoting |

| Module | Description | |--------|-------------| | crypto/ | ChaCha20-Poly1305 AEAD, ECDH, HKDF, MuSig2 (BIP 327) for taproot channels | | message/ | Wire encode/decode for all channel, gossip and control messages | | features/ | Feature flag bitmap management (BOLT 9) | | transport/ | Noise_XK handshake, transport cipher, TCP/WebSocket peer connections, PeerManager | | keys/ | HD derivation, per-commitment secrets (shachain), signing, wallet keys | | script/ | Funding 2-of-2 multisig, commitment outputs, HTLC scripts, revocation, anchors, taproot scripts | | channel/ | Channel state machine, ChannelManager, commitment builder, actions, validation, liquidity ads | | chain/ | ChainMonitor, ChainWatcher, output resolver, closing tx, sweep tx, Electrum backend | | invoice/ | BOLT 11 encoding/decoding, bech32 words, signature verification | | gossip/ | NetworkGraph, Dijkstra pathfinding, gossip sync state machine, SCID encoding | | onion/ | Sphinx crypto, packet construction/processing, hop payloads, failures, blinded paths | | onion-message/ | Onion message construction/processing (carries BOLT 12 and async-payment messages) | | offer/ | BOLT 12 offers: encode/decode, OfferManager invoice_request/invoice flows | | async-payments/ | Hold invoices and AsyncPaymentManager (LSP held-forward, release_held_htlc, wake) | | ffor/ | FFOR Variant D offline receive: signed epoch lifecycle, voucher book, transcript hashes, delegated settlement arithmetic | | interactive-tx/ | Interactive transaction construction for v2 dual-funded opens and splicing | | watchtower/ | Altruist watchtower client: wtwire protocol, justice blobs, tower sessions | | backup/ | Static channel backup (SCB) export/import | | recovery/ | Safety transition layer: atomic persistence, the durable outbound-message outbox, the opt-in hash-chained recovery journal, and the peer_storage Recovery Capsule | | liquidity/ | JIT channel receive (LSP role): intercept SCIDs, held HTLCs, zero-conf open or splice, then forward; the opening fee is skimmed off the delivery for wallets that accept it, or charged to the sender through the invoice hint (hop mode) for wallets that cannot settle a short HTLC | | direct-funding/ | Third-party direct funding: the signed payment request envelope, sealed frames, protocol messages, outstanding-request store, the transport registry with its direct-peer, onion and blind-relay lanes, the receiver engine that turns a payer's offered UTXO into channel funding, and the payer engine that verifies and signs it | | swaps/ | Swaps: the P2WSH HTLC contract, claim/refund transactions, preimage extraction, admission policies, the durable swap ledger, the chain resolver, the wire protocol, and the swap provider engines (reverse: Lightning to on-chain; submarine: on-chain to Lightning) | | l402/ | L402 (Lightning HTTP 402) client: challenge parsing, macaroon reading, paid credentials | | node/ | LightningNode orchestrator, the main protocol-layer entry point | | wallet/ | WalletFundingProvider, adapts the on-chain Wallet for auto-funded opens | | bootstrap/ | DNS seed resolution for discovering initial peers | | advisor/ | Liquidity, fee and channel-suggestion advisors | | storage/ | SQLite persistence backend, channel state serialization | | validation/ | Input validation shared across modules |

beignet/lightning re-exports each of these as a namespace (crypto, message, node, ...). async-payments and watchtower are reachable via their source paths.

Tests

Use Node.js 20 (20.19+) for development and npm ci to install the locked tools. The linter and test runner have newer Node requirements than the library API. CI uses Node.js 20.

The lint configuration keeps the established CommonJS imports, enum aliases, unused catch bindings, any warnings and Chai assertion style. Rules removed from the upgraded linter's recommended preset remain explicitly enabled where they were enforced before. Prettier covers TypeScript semicolons, with the core semicolon rule retained for JavaScript. Typed promise checks remain enabled for every TypeScript file.

npm run test:local         # test:lightning + test:cli at once; no infrastructure needed
npm run test:lightning     # 6200+ Lightning unit tests (parallel), no infrastructure needed
npm run test:cli           # 1350+ CLI + daemon unit tests (parallel), no infrastructure needed
npm run test:conformance   # 250+ official BOLT vector cases (subset of test:lightning)
npm run test:chaos         # recovery kill matrices (parallel), split out of test:lightning
npm run test:sigkill       # process-level SIGKILL chaos matrix (builds dist first)
npm run test:integration   # daemon/Electrum integration (needs an Electrum server)
npm run test:interop       # 190+ cases vs LND/CLN/Eclair (needs Docker)
npm run test:interop:ffor  # FFOR Variant D chain gates on regtest (needs only the bitcoind container)
npm run test:interop:ffor-concurrent  # Concurrent receive process, durable boundary and current-chain qualification (bitcoind regtest)
npm run test:all           # Lightning + CLI + interop (needs Docker + Electrum)

Counts are floors, not snapshots. Run the suites for exact numbers.

test:local is the fast inner loop. test:lightning and test:cli are independent processes and neither saturates the machine alone, so running them at once beats running them in sequence. Measured on 8 cores: 195.6s in sequence before this existed, 106.1s in sequence once test:cli gained --parallel, and 77.1s together. It prints a per-suite summary and, on failure, the failing suite's output.

test:chaos is deliberately not in that bundle. Its cases carry real wall-clock budgets that do not care how loaded the box is (chaosWait defaults to 15s, the quorum barrier to 20s), and sharing the machine ate one of them: running all three concurrently failed with chaosWait timed out with the victim alive while the same suite passes 30/30 on its own. Run it separately. See scripts/run-suites.js for the worker split and why more workers is not better.

The on-chain wallet suites live in tests/*.test.ts and connect to live public Electrum servers, so they need network access and can fail on a server outage rather than on your change. Each script runs npm run build first:

npm run test:wallet        # also test:transaction, test:electrum, test:storage,
                           # test:derivation, test:receive, test:boost
npm test                   # everything: build, on-chain, Lightning, CLI, interop

The on-chain files without a dedicated script (multisig, PSBT, watch-only, descriptors, signet and others) run through mocha directly:

npx mocha --exit -r ts-node/register 'tests/multisig.test.ts'

test:conformance runs the official BOLT test vectors (BOLT 1 bigsize/TLV, BOLT 3 commitments and anchors and per-commitment secrets, BOLT 4 onion/route-blinding/onion-errors, BOLT 7 extended queries, BOLT 8 transport, BOLT 11 invoices, BOLT 12 offers/signatures) under tests/lightning/conformance/.

The interop suite drives beignet against real nodes on Bitcoin regtest.

docker compose -f docker/docker-compose.yml up -d   # wait ~30s for nodes to sync
npm run test:interop

Services in docker/docker-compose.yml:

| Service | Image | Ports | |---------|-------|-------| | bitcoind | Bitcoin Core 31.0 (regtest) | RPC 43782, ZMQ 28334/28335/28336 | | lnd | lightninglabs/lnd:v0.20.0-beta | P2P 9735, REST 8081 | | cln | elementsproject/lightningd:v26.06.1 | CLNRest 3010 | | eclair | 0.14.1, built locally from the release zip (docker/eclair/Dockerfile) | HTTP API 8082 | | electrs | getumbrel/electrs:v0.10.10 | Electrum 60001 |

The LND helpers read LND_REST_HOST / LND_REST_PORT (default 127.0.0.1:8081) and LND_P2P_HOST / LND_P2P_PORT (default 127.0.0.1:9735); the dedicated taproot container reads LND_TAPROOT_REST_HOST / LND_TAPROOT_REST_PORT (default 127.0.0.1:8082) and LND_TAPROOT_P2P_HOST / LND_TAPROOT_P2P_PORT (default 127.0.0.1:9736). Point them at whatever your docker/docker-compose.override.yml publishes, for example LND_REST_PORT=8091 npm run test:interop.

An LND suite that has no usable counterparty skips itself and prints one line naming the suite, the endpoint it probed and the variables that move it ([skip] lnd-jit-receive: LND REST not reachable at 127.0.0.1:8081 (set LND_REST_PORT / LND_REST_HOST), or ... LND reachable but macaroon read failed (docker exec lnd ...)). A skip is invisible in a passing summary, so for a pre-release gate set INTEROP_REQUIRE_LND=1 (and INTEROP_REQUIRE_LND_TAPROOT=1 for the taproot suites): the same condition then fails the suite with that reason instead of skipping it.

Covered per implementation: BOLT 8 handshake and BOLT 1 init/feature negotiation, disconnect/reconnect and ping/pong survival, channel open in both directions, bidirectional payments and payment_secret validation, MPP, SCID aliases, cooperative close, reestablish, gossip sync, inbound connections, anchor channels, anchor force-close with wallet-funded CPFP and HTLC-timeout fee-attach, and crash recovery. Beyond the shared matrix: taproot channel lifecycle vs LND (open, pay both directions, reestablish, coop and force close, penalty, SCB recovery), splice matrix and lease/liquidity-ads flows vs CLN, simple_close vs Eclair, blinded-path payments, and the watchtower client vs an LND tower.

Interop tests are excluded from npm run test:lightning.

Status & limitations

Beignet is under active development. Known gaps and caveats:

| Feature | Status | Detail | |---------|--------|--------| | Mainnet battle-testing | Limited | Interop-tested on regtest, with some flows validated live on mainnet. Exercise caution with large balances. | | Watchtowers | Client only (altruist) | Punishes breaches while you are offline via remote LND altruist towers. Legacy + anchor channels only: taproot channels are not backed up. No server mode. | | LSP / LSPS protocols | Not implemented | No automated inbound liquidity via LSPS0/1/2. Liquidity ads (bLIP-51) cover negotiated leases; otherwise open channels manually. | | Trampoline routing | Not implemented | All route computation is local. | | BOLT 12 offers | Newer | Offers, invoice_request/invoice over onion messages and receive-side settlement work, but the surface is less battle-tested than BOLT 11. Prefer BOLT 11 in production. | | Async payments | LSP-dependent | Hold invoices plus AsyncPaymentManager let an offline receiver be paid, but the receiver's LSP must run the held-forward/wake flow. | | Simple taproot channels | Experimental | Full lifecycle validated against LND v0.20 on regtest, but the feature bit is still in staging upstream. Not recommended for mainnet balances. | | Splicing / dual funding | Partial | Splice-out and splice-in validated live against CLN; v2 dual-funded opens implemented both as initiator and acceptor. CLN-initiated splices, repeat splices and multi-UTXO splice-ins are untested. | | Mobile background | Limited | Works on React Native but has no background sync or push-notification support. |

Recommended safeguards in production:

  • Cap exposure with maxPaymentSats and dailySpendLimitSats. Both count a payment's amount plus its routing-fee cap, so the fee cannot slip past them: the cap is maxFeeSats/maxFeeMsat when you pass one, and 1% of the amount (never below 50 sats) when you do not. Both also cover external on-chain sends (amount plus fee); see the Spending Limits section of src/cli/README.md.
  • Call validatePayment() before every send.
  • Set backupPath for automated database backups, and keep an SCB (beignet backup scb).
  • Keep ~/.beignet and the data directory owner-only. The CLI creates them 0700/0600 and tightens an older config on load; check them again after copying files between hosts.
  • Pass multiple electrumServers for connection redundancy.
  • Configure watchtowers so breaches are punished while you are offline.
  • Monitor node:error events and the /health endpoint.
  • Start with small channels and increase gradually.

React Native

react-native-tcp-socket is a drop-in replacement for net and tls:

{
  "react-native": {
    "net": "react-native-tcp-socket",
    "tls": "react-native-tcp-socket"
  }
}

Documentation

| Document | Contents | |----------|----------| | example/REPL_TESTING.md | Copy-pasteable REPL walkthrough of the full node lifecycle | | docs/AI_AGENT_GUIDE.md | Deployment, monitoring, safety rails, HTTP daemon patterns | | src/lightning/README.md | Protocol-layer reference and usage guide | | docs/ROADMAP.md | Feature roadmap and progress | | docs/RECOVERY-PROTOCOL.md | Proposed replicated state-continuity design | | API reference | Generated typedoc (HTML) |

Support

Open an issue, or reach out on Telegram.

License

MIT