Skip to main content

Accept Bitcoin Payments on Your Website or App with Rizful

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 every few seconds until `settled_at` or `preimage` is present.

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

  while (Date.now() - startTime < timeoutMs) {
    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
    }

    // Wait 3 seconds before next check
    await new Promise((r) => setTimeout(r, 3_000));
  }

  throw new Error("Invoice timed out — 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 every **3 seconds** for a good balance of responsiveness and relay load. 10-minute timeout is reasonable for checkout flows.

---

### 3. Full Create + Poll Flow

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

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

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,
  });

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

  // Step 2: Poll until paid or timed out
  const startTime = Date.now();
  const TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes

  try {
    while (Date.now() - startTime < TIMEOUT_MS) {
      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;
      }
      await new Promise((r) => setTimeout(r, 3_000));
    }
    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 all pending invoices:

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

async function pollAllPending(client) {
  while (true) {
    for (const [paymentHash, order] of pendingInvoices.entries()) {
      // Expire invoices older than 10 minutes
      if (Date.now() - order.createdAt > 10 * 60 * 1000) {
        pendingInvoices.delete(paymentHash);
        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
      }
    }
    await new Promise((r) => setTimeout(r, 3_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.                         |
| Typical polling interval | 3 seconds                                                          |
| 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 code for creating invoices, polling for payment, and listening for notifications — everything an AI coding agent needs to wire up Lightning payments in your app.


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 — your app calls make_invoice, gets back a BOLT11 invoice string and a payment_hash.
  2. Show the invoice — display it as a QR code or payment link for the customer.
  3. Poll for settlement — your app calls lookup_invoice every few seconds until settled_at or preimage appears.
  4. React — 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",
});

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

Poll Until Paid

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

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

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

await new Promise((r) => setTimeout(r, 3_000)); // check every 3 seconds
}

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

Full Create + Poll Example

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

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

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

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

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

const startTime = Date.now();
const TIMEOUT_MS = 10 * 60 * 1000;

try {
while (Date.now() - startTime < TIMEOUT_MS) {
const result = await client.lookupInvoice({ payment_hash });
if (result.settled_at || result.preimage) {
console.log("Paid! Preimage:", result.preimage);
return result;
}
await new Promise((r) => setTimeout(r, 3_000));
}
throw new Error("Invoice expired");
} 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.