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

@candypoets/lnuts

v0.1.6

Published

LNURL-pay to Cashu mint bridge with Nostr integration

Readme

lnuts

Turn any Node server into a full LNURL-pay server with just a few configuration variables!

A full Nostr-ready LNURL-pay server that bridges Lightning payments to Cashu tokens. Allows anyone to claim [email protected] addresses that will receive Cashu tokens via Lightning payments. The server sends both the Cashu proofs back to the Nostr address and the payment confirmation (kind 9735 events) making it fully zap-compatible.

Features

  • LNURL-pay Endpoint: Serve /.well-known/lnurlp/[alias] for Lightning payments
  • Nostr Integration: Claim addresses via Nostr kind 10020 events with optional P2PK locking
  • Cashu Mint Bridge: Automatically mint Cashu tokens when payments are received
  • Token Delivery: Deliver tokens back to users via Nostr kind 9321 events
  • Zap Support: Sends payment confirmations via kind 9735 events for full Nostr zap compatibility
  • Automatic Polling: Continuously polls for paid MintQuotes and delivers tokens
  • Retry Mechanism: Automatic retry of failed token deliveries
  • Admin API: Monitor and manage the system via REST endpoints
  • SQLite Storage: Persistent storage for claims and payments

Architecture

User → Lightning Payment → LNURL-pay → Cashu Mint → Nostr Delivery → User
  1. Users claim [email protected] via Nostr kind 10020 events (supports P2PK pubkey for locking proofs)
  2. Payments come in via LNURL-pay endpoint
  3. Cashu mint generates tokens when payment is confirmed
  4. Tokens are delivered back to user via kind 9321 events
  5. Payment confirmations are broadcast via kind 9735 events for zap compatibility

Installation

npm install @candypoets/lnuts

Configuration

Set up environment variables:

# Required
LNUTS_DOMAIN=mydomain.com
LNUTS_MINT_URL=https://mint.example.com
LNUTS_ADMIN_SECRET_KEY=your_admin_secret_key_or_nsec

# Optional
LNUTS_DATABASE_PATH=./data/lnuts.db
LNUTS_NOSTR_RELAYS=wss://relay.damus.io,wss://relay.nostr.band,wss://nos.lol

Usage with SvelteKit

1. Quick Start with Middleware

The simplest way to add lnuts to your SvelteKit app:

// src/hooks.server.ts
import { createLnutsMiddleware } from '@candypoets/lnuts/server';

export const handle = createLnutsMiddleware();

2. Advanced: Compose with Other Middleware

// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import { withLnuts } from '@candypoets/lnuts/server';

const myAuthMiddleware = async ({ event, resolve }) => {
  // Your auth logic
  return resolve(event);
};

export const handle = sequence(
  withLnuts(),
  myAuthMiddleware
);

3. Create Required Routes

LNURL-pay endpoints are automatically handled by the middleware at:

  • GET /.well-known/lnurlp/[alias] - Returns LNURL metadata
  • GET /.well-known/lnurlp/[alias]/callback - Generates Lightning invoice

Admin endpoints are automatically available at:

  • GET /api/admin/claims - List all claimed addresses
  • GET /api/admin/payments - List all payment sessions
  • GET /api/admin/stats - System statistics

API endpoints for managing claims:

  • POST /api/claims - Submit a Nostr event to claim an alias
  • GET /api/claims/[alias] - Get information about a specific alias
  • GET /api/aliases?pubkey=[pubkey] - Query all aliases owned by a public key

All routes are automatically handled by the middleware - no additional route files needed!

Usage with Node.js / Express

Express Server Integration

import express from 'express';
import { createExpressMiddleware } from '@candypoets/lnuts/server';

const app = express();
const port = 3000;

// Add body parser for JSON
app.use(express.json());

// Apply lnuts middleware - it handles all routes automatically
app.use(createExpressMiddleware({
  // Optional: override environment variables
  domain: 'mydomain.com',
  mintUrl: 'https://mint.example.com',
  adminSecretKey: process.env.LNUTS_ADMIN_SECRET_KEY
}));

// Your other routes
app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

All routes are automatically handled by the middleware - no additional configuration needed!

Handler Class API

Constructor Options

interface LnutsOptions {
  dbPath?: string;           // Path to SQLite database file (default: from env or './lnuts.db')
  mintUrl?: string;          // Cashu mint URL (required)
  nostrRelays?: string[];    // Nostr relays to connect to
  adminSecretKey?: string;   // Nostr private key for signing events (required)
  domain?: string;           // Domain name for LNURL metadata
  autoStart?: boolean;       // Whether to auto-start services (default: true)
}

Creating an Instance

import { LnutsHandler } from '@candypoets/lnuts';

// Using environment variables
const lnuts = new LnutsHandler();

// With custom options
const lnuts = new LnutsHandler({
  domain: 'mydomain.com',
  mintUrl: 'https://mint.example.com',
  adminSecretKey: 'nsec1...' // or hex private key,
  dbPath: './data/lnuts.db',
  autoStart: false  // Manual service start
});

// Manually start services if autoStart is false
await lnuts.start();

Properties

  • db: Direct access to the SQLite database instance
  • wallet: Cashu wallet instance for mint operations
  • mint: Cashu mint client instance
  • options: Configuration options used by the handler
  • servicePubkey: Derived public key for Nostr events
  • pool: SimplePool instance for Nostr operations
  • handle: SvelteKit-compatible handle function

Methods

  • start(): Manually start all services (polling, retry scheduler)
  • stop(): Stop all services
  • pollPayments(): Manually trigger payment polling
  • retryFailedDeliveries(): Manually retry failed token deliveries
  • clone(options): Create a new handler instance with modified options

Database Schema

Claims Table

  • alias: The claimed username
  • pubkey: Nostr pubkey of the claimer
  • p2pk_pubkey: Optional P2PK pubkey for token locking
  • relay: Preferred relay for delivery
  • claimed_at: Timestamp of claim

Payments Table

  • id: Unique payment ID
  • alias: Associated username
  • quote_id: Cashu mint quote ID
  • amount: Payment amount in sats
  • invoice: Lightning invoice
  • comment: Optional payment comment
  • status: pending | paid | minted | delivered | failed
  • created_at: Payment creation timestamp
  • updated_at: Last update timestamp

Failed Deliveries Table

  • id: Unique delivery ID
  • payment_id: Reference to payment
  • error: Error message
  • retry_count: Number of retry attempts
  • created_at: Failure timestamp

Automatic Services

Payment Polling

The handler automatically polls the Cashu mint for paid invoices:

  • Default interval: 1 second
  • Automatically mints tokens when payment is confirmed
  • Delivers tokens via Nostr to the recipient

Retry Scheduler

Failed token deliveries are automatically retried:

  • Default interval: 5 minutes
  • Exponential backoff for repeated failures
  • Maximum retry attempts before marking as permanently failed

API Endpoints

Submit Claim Event

POST /api/claims

Submit a Nostr kind 10020 event to claim an alias:

curl -X POST http://localhost:3000/api/claims \
  -H "Content-Type: application/json" \
  -d '{
    "kind": 10020,
    "tags": [
      ["name", "alice"],
      ["relay", "wss://relay.damus.io"],
      ["mint", "https://mint.example.com"],
      ["p2pk", "02abc..."]
    ],
    "content": "",
    "pubkey": "user_pubkey",
    "created_at": 1234567890,
    "id": "event_id",
    "sig": "signature"
  }'

Response:

{
  "status": "success",
  "data": {
    "alias": "alice",
    "pubkey": "user_pubkey",
    "relay": "wss://relay.damus.io",
    "p2pk_pubkey": "02abc..."
  }
}

Get Alias Information

GET /api/claims/[alias]

Get information about a specific claimed alias:

curl http://localhost:3000/api/claims/alice

Response:

{
  "status": "success",
  "data": {
    "alias": "alice",
    "pubkey": "user_pubkey",
    "relay": "wss://relay.damus.io",
    "p2pk_pubkey": "02abc...",
    "claimed_at": "2024-01-01T00:00:00Z"
  }
}

Query Aliases by Public Key

GET /api/aliases?pubkey=[pubkey]

Get all aliases owned by a specific public key:

curl http://localhost:3000/api/aliases?pubkey=02abc123...

Response:

{
  "status": "success",
  "data": [
    {
      "alias": "alice",
      "relay": "wss://relay.damus.io",
      "p2pk_pubkey": "02abc...",
      "claimed_at": "2024-01-01T00:00:00Z"
    },
    {
      "alias": "bob",
      "relay": "wss://nos.lol",
      "p2pk_pubkey": null,
      "claimed_at": "2024-01-02T00:00:00Z"
    }
  ]
}

Client Utilities

The library provides utility functions to help clients interact with the lnuts API:

constructClaimEvent

Creates an unsigned Nostr kind 10020 event for claiming an alias:

import { constructClaimEvent } from '@candypoets/lnuts/utils';

const claimEvent = constructClaimEvent(
  'alice',                           // alias to claim
  'pubkey_hex',                      // your Nostr public key
  'https://mint.example.com',        // Cashu mint URL
  ['wss://relay.damus.io'],          // optional: preferred relays
  '02abc...'                         // optional: P2PK pubkey for locking
);

// Sign the event with your Nostr private key
const signedEvent = finalizeEvent(claimEvent, privateKey);

Parameters:

  • alias: The alias to claim (1-30 chars, alphanumeric and underscores only)
  • pubkey: Nostr public key of the claimer
  • mintUrl: Cashu mint URL to associate with the claim
  • relays: (optional) Array of preferred relay URLs for token delivery
  • p2pkPubkey: (optional) P2PK public key for locking tokens

Returns an unsigned Nostr event ready for signing.

postClaimEvent

Submits a signed claim event to the lnuts API:

import { postClaimEvent } from '@candypoets/lnuts/utils';
import { finalizeEvent } from 'nostr-tools';

// Create and sign the claim event
const unsignedEvent = constructClaimEvent('alice', pubkey, mintUrl);
const signedEvent = finalizeEvent(unsignedEvent, privateKey);

// Submit the claim
try {
  const result = await postClaimEvent(
    signedEvent,
    'mydomain.com'  // optional: defaults to LNUTS_DOMAIN env var
  );
  
  if (result.status === 'success') {
    console.log('Alias claimed successfully!');
  } else {
    console.error('Claim failed:', result.reason);
  }
} catch (error) {
  console.error('Error posting claim:', error);
}

Parameters:

  • event: The signed Nostr kind 10020 claim event
  • domain: (optional) The domain to claim the handle on

Returns a promise resolving to:

{
  status: 'success' | 'error',
  message?: string,  // Success message
  reason?: string    // Error reason
}

Complete Example

Here's a complete example of claiming an alias:

import { constructClaimEvent, postClaimEvent } from '@candypoets/lnuts/utils';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';

// Generate or use existing Nostr keys
const secretKey = generateSecretKey();
const pubkey = getPublicKey(secretKey);

// Create the claim event
const claimEvent = constructClaimEvent(
  'alice',
  pubkey,
  'https://mint.example.com',
  ['wss://relay.damus.io', 'wss://nos.lol'],
  '02abc123...'  // optional P2PK pubkey
);

// Sign the event
const signedEvent = finalizeEvent(claimEvent, secretKey);

// Submit the claim
const result = await postClaimEvent(signedEvent, 'mydomain.com');

if (result.status === 'success') {
  console.log(`Successfully claimed [email protected]!`);
  // The alias will now receive Lightning payments that get converted to Cashu tokens
}

Nostr Events

Claim Event (Kind 10020)

Users claim addresses by publishing this event:

{
  "kind": 10020,
  "tags": [
    ["name", "alice"],
    ["relay", "wss://relay.damus.io"],
    ["mint", "https://mint.example.com"],
    ["p2pk", "02abc..."]  // Optional: P2PK pubkey for token locking
  ],
  "content": "",
  "pubkey": "user_pubkey",
  "created_at": 1234567890
}

Token Delivery (Kind 9321)

Tokens are delivered via encrypted direct messages:

{
  "kind": 9321,
  "tags": [
    ["p", "recipient_pubkey"],
    ["amount", "1000"],
    ["mint", "https://mint.example.com"]
  ],
  "content": "encrypted_cashu_token",
  "pubkey": "service_pubkey",
  "created_at": 1234567890
}

Security Considerations

Admin Secret Key

  • Store securely, never commit to version control
  • Use environment variables or secure key management
  • Supports both nsec (bech32) and hex format

Database Security

  • Default location is configurable via dbPath
  • Ensure proper file permissions
  • Regular backups recommended

Development

# Install dependencies
npm install

# Run with environment variables
LNUTS_DOMAIN=localhost \
LNUTS_MINT_URL=https://testnut.cashu.space \
LNUTS_ADMIN_SECRET_KEY=nsec1... \
node server.js

# For SvelteKit development
npm run dev

Testing

# Test LNURL endpoint
curl http://localhost:3000/.well-known/lnurlp/alice

# Test callback
curl "http://localhost:3000/.well-known/lnurlp/alice/callback?amount=1000000"

# Submit a claim (requires valid Nostr event)
curl -X POST http://localhost:3000/api/claims \
  -H "Content-Type: application/json" \
  -d '{"kind":10020,"tags":[["name","alice"]],...}'

# Get alias information
curl http://localhost:3000/api/claims/alice

# Query aliases by public key
curl http://localhost:3000/api/aliases?pubkey=02abc...

# Test admin stats
curl http://localhost:3000/api/admin/stats

Examples

See the examples/ directory for complete implementations:

  • SvelteKit routes
  • Express server
  • Admin dashboard

Troubleshooting

Services Not Starting

  • Check that mintUrl and adminSecretKey are properly configured
  • Verify the Cashu mint is accessible
  • Check database file permissions

Tokens Not Delivering

  • Verify Nostr relays are accessible
  • Check recipient's relay preferences
  • Review failed deliveries in the database

LNURL Not Working

  • Ensure .well-known/lnurlp routes are properly configured
  • Verify domain configuration matches actual hostname
  • Check that aliases exist in claims table

License

MIT