Paid relay

NostrFeed has two official relays. Do not replace the free one with the paid one.

RoleURLWho can write
Free publicwss://relay.nostrfeed.comanyone
Paid writewss://paid.nostrfeed.compubkey that paid 2100 sats once

Keep DMs, zaps, NWC, bunker, and kind 10050 on the free relay. Use the paid relay as an extra write outbox after admission. Reads on the paid relay are open.

Do this in the NostrFeed app. Do not send the user to the website to pay.

Web fallback only: paid.nostrfeed.com/invoices

1. Discovery (once at app start)

GET https://api.getzap.me/v1/connect
disc.relay                  // wss://relay.nostrfeed.com  — always connect
disc.inbox_relay            // same, DMs / NWC / bunker
disc.paid_relay             // wss://paid.nostrfeed.com
disc.admission.url          // https://paid.nostrfeed.com
disc.admission.check        // https://paid.nostrfeed.com/admissions/check/{pubkey}
disc.admission.invoices     // https://paid.nostrfeed.com/invoices
disc.admission.amount_sats  // 2100
disc.admission.once         // true

Identity pack also has it: GET /identity?pubkey= returns relays.paid, admission, and a kind 10002 template.

2. Pay inside the client (no webpage)

Show a checkbox for the terms. Then create the invoice with JSON. Show QR / bolt11 / WebLN / NWC in your UI. Poll until admitted. Never open paid.nostrfeed.com in a browser.

const RELAY_HTTP = "https://paid.nostrfeed.com";

async function ensurePaidRelay(hexPubkey) {
  const checkUrl = RELAY_HTTP + "/admissions/check/" + hexPubkey;
  let { userAdmitted } = await fetch(checkUrl).then((r) => r.json());
  if (userAdmitted) return;

  const inv = await fetch(RELAY_HTTP + "/invoices", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Accept": "application/json" },
    body: JSON.stringify({ pubkey: hexPubkey, tosAccepted: true, feeSchedule: "admission" }),
  }).then((r) => r.json());

  if (inv.userAdmitted) return;

  // in-app: QR of lightning:inv.bolt11, copy, WebLN, NWC
  showInvoice(inv.bolt11, inv.amount_sats);

  if (window.webln) {
    await window.webln.enable();
    await window.webln.sendPayment(inv.bolt11);
  }

  while (true) {
    await sleep(3000);
    const st = await fetch(inv.status_url).then((r) => r.json());
    const again = await fetch(checkUrl).then((r) => r.json());
    if (st.status === "completed" || again.userAdmitted) return;
    if (st.status === "expired") throw new Error("invoice expired");
  }
}

connectRelay(disc.relay);
const pack = await fetch(BASE + "/identity?pubkey=" + hexPubkey).then((r) => r.json());
await signAndPublish(pack.setup, disc.relay);
await ensurePaidRelay(hexPubkey);
connectRelay(disc.paid_relay);
publishNotesTo([disc.relay, disc.paid_relay]);

Invoice JSON looks like:

{
  "ok": true,
  "userAdmitted": false,
  "id": "payment-hash",
  "bolt11": "lnbc21u1…",
  "amount_sats": 2100,
  "expires_at": "2026-08-21T07:15:00.000Z",
  "status": "pending",
  "relay": "wss://paid.nostrfeed.com",
  "status_url": "https://paid.nostrfeed.com/invoices/{id}/status",
  "check": "https://paid.nostrfeed.com/admissions/check/{pubkey}"
}

If the key already paid, POST returns userAdmitted: true and no bolt11. CORS is open for https://www.nostrfeed.com.

Fee info without creating an invoice:

GET https://paid.nostrfeed.com/invoices
Accept: application/json

3. Relay list events

Kind 10002 from pack.setup.kind10002:

{
  "kind": 10002,
  "tags": [
    ["r", "wss://relay.nostrfeed.com"],
    ["r", "wss://paid.nostrfeed.com", "write"]
  ]
}

Kind 10050 (DM inbox) stays only the free relay:

{
  "kind": 10050,
  "tags": [["relay", "wss://relay.nostrfeed.com"]]
}

Publish those signed events to the free relay first. After admission, also publish notes to the paid relay.

4. Admission API

WhatCall
Pay pageGET https://paid.nostrfeed.com/invoices
Admitted?GET /admissions/check/{hex}{"userAdmitted": true|false}
Create invoicePOST /invoices form: pubkey, tosAccepted=yes, feeSchedule=admission
Invoice statusGET /invoices/{id}/statuspending|completed|expired
NIP-11GET / with Accept: application/nostr+json
POST https://paid.nostrfeed.com/invoices
Content-Type: application/x-www-form-urlencoded

pubkey=npub1…&tosAccepted=yes&feeSchedule=admission

NIP-11 has limitation.payment_required: true and fees.admission: 2100000 msats.

Unpaid writes are rejected with blocked: pubkey not admitted. Kind 9735 zap receipts do not need admission.

5. What the client must not do

Open pay page