Skip to main content

Accept Bitcoin Payments on Your Website or App with Rizful

Backend guide

This page is for the backend payment code: creating a BOLT11 invoice, saving the payment_hash, checking settlement, and marking the order paid. It is written for Node.js web backends such as Next.js, Remix, Astro, SvelteKit, Nuxt / Nitro, Express, Fastify, NestJS, Hono, Koa, AdonisJS, and similar frameworks.

For the checkout UI and customer-facing payment wizard, use Accept Crypto On Your Website For Free (No KYC) after this backend flow is working.

Quick Start

Three steps to get going

1. Sign up — create a free account at Rizful.com

2. Get a receive-only NWC code — in Rizful, go to Settings → NWC → Receive-only NWC Code and copy the connection string. See full instructions here.

3. Give your LLM or agent the instructions — copy the instructions below and paste them into your AI coding assistant. They contain everything needed to build the integration.

# Rizful NWC Integration — Agent Instructions

> Paste this file into your LLM or coding agent to give it everything it needs to add Lightning Bitcoin payments to your app using Rizful and Nostr Wallet Connect (NWC).

---

## What Is This?

[Rizful.com](https://rizful.com) is a hosted Lightning wallet that exposes a **Nostr Wallet Connect (NWC)** API. NWC lets your app create Bitcoin Lightning invoices, poll for payment confirmation, and react the moment an invoice is paid — all via a simple connection string and a small SDK.

**No Lightning node required. No backend server required. Just a connection string.**

---

## Prerequisites

1. **Sign up at [Rizful.com](https://rizful.com)** — free, takes 30 seconds.
2. **Generate a receive-only NWC connection string** — in Rizful, open Settings → NWC → _Receive-only NWC Code_. Copy the string (starts with `nostr+walletconnect://`).
3. **Install the SDK:**
   ```bash
   npm install @getalby/sdk
   ```

> **Security rule:** Use a _receive-only_ NWC code for any merchant/checkout integration. It can only create invoices and check their status — it cannot send funds. Only use a full send-and-receive code if your app explicitly needs to send payments (e.g. payouts).

---

## NWC Connection String Format

```
nostr+walletconnect://<wallet-pubkey>?relay=<relay-url>&secret=<client-secret>
```

| Part            | Description                                     |
| --------------- | ----------------------------------------------- |
| `wallet-pubkey` | Hex pubkey of the Rizful wallet                 |
| `relay`         | Nostr relay URL (e.g. `wss://relay.rizful.com`) |
| `secret`        | Your client keypair secret — keep this private  |

The SDK handles all parsing. You just pass the full string to `NWCClient`.

---

## Core Operations

### 1. Create an Invoice (`make_invoice`)

```js
import { NWCClient } from "@getalby/sdk";

const client = new NWCClient({
  nostrWalletConnectUrl: "nostr+walletconnect://...your-rizful-nwc-code...",
});

// amount is in millisatoshis (msats). 1 sat = 1000 msats.
const { invoice, payment_hash } = await client.makeInvoice({
  amount: 1_000_000, // 1000 sats
  description: "Order #1234", // memo shown to payer
  expiry: 600, // optional: seconds until invoice expires (default varies)
});

console.log("BOLT11 invoice:", invoice); // share this with the customer
console.log("Payment hash:", payment_hash); // use this to poll for settlement
```

**Parameters:**

- `amount` — amount in **millisatoshis** (msats). Multiply sats × 1000.
- `description` — memo string visible to the payer.
- `expiry` — optional expiry in seconds.

**Returns:**

- `invoice` — BOLT11 Lightning invoice string (encode as QR code or payment link for the customer).
- `payment_hash` — unique identifier for this invoice; use it to poll settlement.

---

### 2. Poll for Payment (`lookup_invoice`)

Check whether an invoice has been paid. Poll quickly at first, then slow down as the invoice ages, and stop as soon as the invoice expires.

```js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function nextLookupDelayMs(elapsedMs) {
  if (elapsedMs < 30_000) return 1_000; // first 30 seconds: every second
  if (elapsedMs < 2 * 60 * 1000) return 3_000;
  if (elapsedMs < 5 * 60 * 1000) return 6_000;
  return 12_000;
}

async function waitForPayment(
  client,
  payment_hash,
  expiresAtMs = Date.now() + 10 * 60 * 1000,
) {
  const startTime = Date.now();

  while (Date.now() < expiresAtMs) {
    const result = await client.lookupInvoice({ payment_hash });

    // Invoice is settled when either settled_at (timestamp) or preimage is present
    const isPaid = !!(result.settled_at || result.preimage);

    if (isPaid) {
      console.log("Payment confirmed! Preimage:", result.preimage);
      return result; // ← mark order as paid here
    }

    const elapsedMs = Date.now() - startTime;
    const waitMs = Math.min(nextLookupDelayMs(elapsedMs), expiresAtMs - Date.now());
    if (waitMs > 0) await sleep(waitMs);
  }

  throw new Error("Invoice expired — no payment received");
}
```

**Parameters:**

- `payment_hash` — the hash returned by `makeInvoice`.

**Settlement fields in the response:**

- `result.settled_at` — Unix timestamp when the invoice was paid (present when paid).
- `result.preimage` — payment preimage proving payment (present when paid).

> Poll immediately, then once per second for the first 30 seconds, then back off to 3, 6, and 12 seconds. Always stop once the invoice expires so your app does not run into wallet or relay rate limits.

**Why this matters:** every `lookup_invoice` call is a network request to an NWC wallet service and relay. Endless fast polling can waste server resources and trigger provider rate limits. During checkout, that can hurt the exact users who are trying to pay: old unpaid invoices may keep consuming request quota in the background while new customers are waiting for confirmation.

Use a bounded polling loop: start fast while the customer is most likely to pay, slow down as the invoice gets older, and stop completely when the invoice expires.

---

### 3. Full Create + Poll Flow

```js
import { NWCClient } from "@getalby/sdk";

const NWC_URL = "nostr+walletconnect://...your-rizful-nwc-code...";
const INVOICE_EXPIRY_SECONDS = 10 * 60;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function nextLookupDelayMs(elapsedMs) {
  if (elapsedMs < 30_000) return 1_000;
  if (elapsedMs < 2 * 60 * 1000) return 3_000;
  if (elapsedMs < 5 * 60 * 1000) return 6_000;
  return 12_000;
}

async function createAndPollInvoice(amountMsats, memo) {
  const client = new NWCClient({ nostrWalletConnectUrl: NWC_URL });

  // Step 1: Create the invoice
  const { invoice, payment_hash } = await client.makeInvoice({
    amount: amountMsats,
    description: memo,
    expiry: INVOICE_EXPIRY_SECONDS,
  });

  console.log("Show this to the customer:", invoice);

  // Step 2: Poll until paid or expired
  const startTime = Date.now();
  const expiresAtMs = startTime + INVOICE_EXPIRY_SECONDS * 1000;

  try {
    while (Date.now() < expiresAtMs) {
      const result = await client.lookupInvoice({ payment_hash });
      if (result.settled_at || result.preimage) {
        console.log("Paid! Preimage:", result.preimage);
        // ← YOUR LOGIC HERE: mark order as paid, send confirmation, unlock access, etc.
        return result;
      }

      const elapsedMs = Date.now() - startTime;
      const waitMs = Math.min(nextLookupDelayMs(elapsedMs), expiresAtMs - Date.now());
      if (waitMs > 0) await sleep(waitMs);
    }
    throw new Error("Invoice expired — no payment received");
  } finally {
    client.close();
  }
}

// Usage
createAndPollInvoice(1_000_000, "Order #1234")
  .then((result) => console.log("Order settled:", result.preimage))
  .catch((err) => console.error("Payment failed:", err.message));
```

---

### 4. Listen for Notifications (Optional — Belt-and-Suspenders)

Rizful publishes NWC notification events to the Nostr relay when a payment arrives. You can subscribe to these for near-instant notification, running **alongside** your polling loop as a belt-and-suspenders approach.

**Install nostr-tools:**

```bash
npm install nostr-tools
```

```js
import { SimplePool } from "nostr-tools";

const NWC_URL = "nostr+walletconnect://...your-rizful-nwc-code...";

function parseNwcUri(uri) {
  const u = new URL(uri.replace("nostr+walletconnect://", "http://"));
  return {
    walletPubkey: u.hostname,
    relayUrl: u.searchParams.get("relay"),
  };
}

function listenForPayments(onPayment) {
  const { walletPubkey, relayUrl } = parseNwcUri(NWC_URL);
  if (!relayUrl) throw new Error("No relay URL found in NWC URI");

  const pool = new SimplePool();

  // NWC notification event kinds:
  // 23196 = NIP-04 encrypted notification
  // 23197 = NIP-44 encrypted notification
  const sub = pool.subscribeMany(
    [relayUrl],
    [{ kinds: [23196, 23197], authors: [walletPubkey] }],
    {
      onevent(event) {
        const text = event.content || "";
        // Extract payment hash (64-char hex) or BOLT11 invoice from the event
        const paymentHash = (text.match(/[a-f0-9]{64}/i) || [])[0];
        const invoice = (text.match(/lnbc[0-9a-z]+/i) || [])[0];
        if (paymentHash || invoice) {
          onPayment({ paymentHash, invoice, event });
        }
      },
    },
  );

  // Returns a cleanup function — call it to unsubscribe
  return () => sub.close();
}

// Usage: run this in parallel with your polling loop
const stopListening = listenForPayments(({ paymentHash, invoice }) => {
  console.log("Payment notification received:", { paymentHash, invoice });
  // Look up the matching order by paymentHash or invoice string and mark it as paid
});

// Call stopListening() to unsubscribe when done
```

**When to use notifications:**

- When you need sub-second settlement confirmation.
- When handling high volume and want to reduce `lookup_invoice` polling frequency.
- **Always run polling in parallel** — notifications can be missed if the relay hiccups.

**For most apps, polling alone is sufficient.** Start with polling; add notifications later if needed.

---

## Handling Multiple Pending Invoices

For checkout flows with concurrent orders, maintain a map of `payment_hash → order` and run a single polling loop over only the invoices that are due for another lookup:

```js
const pendingInvoices = new Map();
// payment_hash → { orderId, createdAt, expiresAt, nextLookupAt }

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function nextLookupDelayMs(elapsedMs) {
  if (elapsedMs < 30_000) return 1_000;
  if (elapsedMs < 2 * 60 * 1000) return 3_000;
  if (elapsedMs < 5 * 60 * 1000) return 6_000;
  return 12_000;
}

async function pollAllPending(client) {
  while (true) {
    for (const [paymentHash, order] of pendingInvoices.entries()) {
      const now = Date.now();

      if (now >= order.expiresAt) {
        pendingInvoices.delete(paymentHash);
        continue;
      }

      if (now < order.nextLookupAt) continue;

      const result = await client.lookupInvoice({ payment_hash: paymentHash });
      if (result.settled_at || result.preimage) {
        pendingInvoices.delete(paymentHash);
        markOrderAsPaid(order.orderId, result.preimage); // ← your logic
      } else {
        const elapsedMs = Date.now() - order.createdAt;
        order.nextLookupAt = Date.now() + nextLookupDelayMs(elapsedMs);
      }
    }
    await sleep(1_000);
  }
}
```

---

## Error Handling

```js
try {
  const result = await client.makeInvoice({
    amount: 1_000_000,
    description: "test",
  });
} catch (err) {
  // NWC errors have a `code` field
  if (err.code === "NOT_IMPLEMENTED") {
    console.error("This NWC code does not support this method");
  } else if (err.code === "UNAUTHORIZED") {
    console.error("NWC code is invalid or expired");
  } else {
    console.error("Unexpected error:", err.message);
  }
}
```

Common error codes:

- `NOT_IMPLEMENTED` — the NWC method is not allowed on this connection (e.g. trying to send from a receive-only code).
- `UNAUTHORIZED` — invalid or expired NWC connection string.
- `QUOTA_EXCEEDED` — the connection has hit its spending/receiving limit.
- `INTERNAL` — wallet-side error; retry after a short delay.

---

## Key Facts for the Agent

| Fact                     | Value                                                              |
| ------------------------ | ------------------------------------------------------------------ |
| Invoice amount unit      | millisatoshis (msats). 1 sat = 1000 msats.                         |
| Polling cadence          | Immediate, then 1s for 30s, then 3s / 6s / 12s until expiry        |
| Typical invoice timeout  | 600 seconds (10 minutes)                                           |
| Settlement signals       | `result.settled_at` (timestamp) OR `result.preimage` (hex string)  |
| Notification event kinds | `23196` (NIP-04) and `23197` (NIP-44)                              |
| Receive-only methods     | `make_invoice`, `lookup_invoice`                                   |
| Send-and-receive adds    | `pay_invoice`, `get_balance`, `list_transactions`, and more        |
| SDK package              | `@getalby/sdk` (`NWCClient`)                                       |
| Notification library     | `nostr-tools` (`SimplePool`)                                       |
| NWC spec                 | [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) |

---

## Reference: SDK Methods

```js
// All methods are async and return Promises

// Create a Lightning invoice
client.makeInvoice({ amount, description, expiry? })
// → { invoice, payment_hash, ... }

// Check if an invoice was paid
client.lookupInvoice({ payment_hash }) // or { invoice }
// → { settled_at?, preimage?, amount, description, ... }

// Send a payment (requires send-and-receive NWC code)
client.payInvoice({ invoice })
// → { preimage }

// Get wallet balance (requires send-and-receive NWC code)
client.getBalance()
// → { balance } // in msats

// List transactions
client.listTransactions({ limit?, offset?, type? })
// → { transactions: [...] }

// Always close the client when done to release relay connections
client.close()
```

---

## Quick Reference: What to Do When a Payment Arrives

1. `result.preimage` is your **proof of payment** — store it in your database.
2. Use `result.settled_at` (Unix timestamp) as the payment time.
3. Match the payment to an order via `payment_hash` (the hash you stored when creating the invoice).
4. Mark the order as paid, send a confirmation email, unlock the content, or whatever your app requires.

The instructions above contain the NWC connection string format, all API methods, sample Node.js backend code for creating invoices, polling for payment, and listening for notifications — everything an AI coding agent needs to wire up Lightning payments on your server.


How It Works

Rizful uses Nostr Wallet Connect (NWC) — a standard protocol that lets your app talk to a Lightning wallet via a connection string. The core loop is:

  1. Create an invoice on the server — your backend calls make_invoice, gets back a BOLT11 invoice string and a payment_hash.
  2. Return the invoice to the frontend — your UI displays it as a QR code, copy button, payment link, or the payment wizard from Accept Crypto On Your Website For Free (No KYC).
  3. Poll for settlement on the server — your backend calls lookup_invoice until settled_at or preimage appears, starting quickly and then backing off until the invoice expires.
  4. React on the server — mark the order as paid, unlock content, send a receipt, or whatever your app needs.

This is exactly how BLFS (Bitcoin Lightning For Shopify) works in production.

Use a receive-only NWC code for merchant integrations

A receive-only NWC code can only create invoices and check their status — it cannot send funds. Even if the code is leaked, your balance is safe. Only use a full send-and-receive code if your app needs to send payments (e.g. payouts).


Install the SDK

npm install @getalby/sdk

Create an Invoice

import { NWCClient } from "@getalby/sdk";

const client = new NWCClient({
nostrWalletConnectUrl: "nostr+walletconnect://...your-rizful-nwc-code...",
});

// amount is in millisatoshis (msats). 1 sat = 1000 msats.
const { invoice, payment_hash } = await client.makeInvoice({
amount: 1_000_000, // 1000 sats
description: "Order #1234",
expiry: 600, // optional: seconds until invoice expires
});

console.log("Invoice (show to customer):", invoice);
console.log("Payment hash (save this):", payment_hash);

Poll Until Paid

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function nextLookupDelayMs(elapsedMs) {
if (elapsedMs < 30_000) return 1_000; // first 30 seconds: every second
if (elapsedMs < 2 * 60 * 1000) return 3_000;
if (elapsedMs < 5 * 60 * 1000) return 6_000;
return 12_000;
}

async function waitForPayment(
client,
payment_hash,
expiresAtMs = Date.now() + 10 * 60 * 1000,
) {
const startTime = Date.now();

while (Date.now() < expiresAtMs) {
const result = await client.lookupInvoice({ payment_hash });

if (result.settled_at || result.preimage) {
// ← mark the order as paid here
return result;
}

const elapsedMs = Date.now() - startTime;
const waitMs = Math.min(nextLookupDelayMs(elapsedMs), expiresAtMs - Date.now());
if (waitMs > 0) await sleep(waitMs);
}

throw new Error("Invoice expired — no payment received");
}

This keeps checkout responsive right after the invoice is created, then reduces lookup_invoice calls as the invoice gets older. Always stop polling when the invoice expires so your app does not run into wallet or relay rate limits.

Avoid endless fast polling

Every lookup_invoice call is a network request to an NWC wallet service and relay. If your app checks too often, or keeps checking forever after the invoice is no longer payable, it can waste server resources and trigger provider rate limits. That is especially painful during checkout: the customer may be ready to pay, but your app could be throttled because old invoices are still being checked in the background.

Use a bounded polling loop: start fast while the customer is most likely to pay, slow down as the invoice gets older, and stop completely when the invoice expires.


Full Create + Poll Example

import { NWCClient } from "@getalby/sdk";

const NWC_URL = "nostr+walletconnect://...your-rizful-nwc-code...";
const INVOICE_EXPIRY_SECONDS = 10 * 60;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function nextLookupDelayMs(elapsedMs) {
if (elapsedMs < 30_000) return 1_000;
if (elapsedMs < 2 * 60 * 1000) return 3_000;
if (elapsedMs < 5 * 60 * 1000) return 6_000;
return 12_000;
}

async function createAndPollInvoice(amountMsats, memo) {
const client = new NWCClient({ nostrWalletConnectUrl: NWC_URL });

const { invoice, payment_hash } = await client.makeInvoice({
amount: amountMsats,
description: memo,
expiry: INVOICE_EXPIRY_SECONDS,
});

console.log("Show to customer:", invoice);

const startTime = Date.now();
const expiresAtMs = startTime + INVOICE_EXPIRY_SECONDS * 1000;

try {
while (Date.now() < expiresAtMs) {
const result = await client.lookupInvoice({ payment_hash });
if (result.settled_at || result.preimage) {
console.log("Paid! Preimage:", result.preimage);
return result;
}

const elapsedMs = Date.now() - startTime;
const waitMs = Math.min(nextLookupDelayMs(elapsedMs), expiresAtMs - Date.now());
if (waitMs > 0) await sleep(waitMs);
}
throw new Error("Invoice expired — no payment received");
} finally {
client.close();
}
}

createAndPollInvoice(1_000_000, "Order #1234")
.then((r) => console.log("Order paid — preimage:", r.preimage))
.catch((e) => console.error(e));

Belt-and-Suspenders: Add Notification Subscriptions

Polling is reliable on its own. For sub-second confirmation or high-volume setups, you can also subscribe to NWC notification events from the Rizful relay — and run both in parallel.

npm install nostr-tools
import { SimplePool } from "nostr-tools";

const NWC_URL = "nostr+walletconnect://...your-rizful-nwc-code...";

function parseNwcUri(uri) {
const u = new URL(uri.replace("nostr+walletconnect://", "http://"));
return {
walletPubkey: u.hostname,
relayUrl: u.searchParams.get("relay"),
};
}

function listenForPayments(onPayment) {
const { walletPubkey, relayUrl } = parseNwcUri(NWC_URL);
if (!relayUrl) throw new Error("No relay URL in NWC URI");

const pool = new SimplePool();

// kinds 23196 (NIP-04) and 23197 (NIP-44) are NWC notification events
const sub = pool.subscribeMany(
[relayUrl],
[{ kinds: [23196, 23197], authors: [walletPubkey] }],
{
onevent(event) {
const text = event.content || "";
const paymentHash = (text.match(/[a-f0-9]{64}/i) || [])[0];
const invoice = (text.match(/lnbc[0-9a-z]+/i) || [])[0];
if (paymentHash || invoice) {
onPayment({ paymentHash, invoice, event });
}
},
},
);

return () => sub.close(); // call this to unsubscribe
}

// Run alongside your polling loop
const stopListening = listenForPayments(({ paymentHash }) => {
console.log("Payment notification:", paymentHash);
// Find the matching order and mark it as paid
});

In production, BLFS runs polling and notification subscriptions in parallel — notifications catch payments instantly, polling acts as the fallback. For most apps, polling alone is enough to start.


What the Instructions Include

The copyable instructions at the top of this page contain:

  • NWC connection string format and field reference
  • Full make_invoice and lookup_invoice API reference with parameters
  • Complete Node.js examples for all three patterns (create, poll, notify)
  • Multi-invoice polling loop for concurrent checkouts
  • Error handling and common error codes
  • A quick-reference table of key facts (amount units, timeouts, event kinds, etc.)

Copy and paste them into ChatGPT, Claude, Cursor, Windsurf, or any other LLM to give it everything it needs to build your integration.