Recurring Payments on Bitcoin Cash — Two Working Approaches (No Covenants, No OP_RETURN)

We’ve been seeing a lot of attempts at recurring payments on BCH, most relying on OP_RETURN metadata or waiting for covenants. We took a different approach and implemented two working solutions — both live and open source. Neither requires new opcodes.

Repo: https://github.com/00-Protocol/00-Wallet
Live app: 00 Sub — 0penw0rld

The Problem

Crypto is push-only — nobody can pull from your wallet. Every recurring payment attempt needs to solve: how do you authorize N future payments without giving away your private key, and without knowing future UTXOs?

Solution 1: Pre-signed nLockTime Chain (Fixed BCH — trustless, wallet offline)

The key insight: you CAN know future UTXOs if you create them yourself.

  1. Payer sends total subscription amount to self in a funding TX
  2. Pre-signs a chain of N transactions, each spending the previous one’s change output
  3. Each TX has nLockTime set to the next payment date
  4. Receiver holds the pre-signed TXs and broadcasts each when its locktime expires
Funding TX → 12,000 sats (self)
  │
  ├─ TX₁ (nLockTime: Apr 1)  → 1000 to merchant + 11000 change
  │   │
  │   ├─ TX₂ (nLockTime: May 1)  → 1000 to merchant + 10000 change
  │   │   │
  │   │   ├─ TX₃ (nLockTime: Jun 1)  → 1000 to merchant + 9000 change
  │   │       ...

The critical detail — input sequence must be 0xFFFFFFFE , not 0xFFFFFFFF :

const input = {
  txidLE: h2b(prevTxid).reverse(),
  vout: prevVout,
  value: inputValue,
  sequence: 0xFFFFFFFE,  // enables nLockTime enforcement
  scriptSig: new Uint8Array(0)
};

const hash = bchSighash(2, locktime, [input], outputs, 0, myScript, inputValue);
const sig = secp256k1.sign(hash, _privKey, { lowS: true });

Each TX’s txid is computed locally before broadcast via dsha256(serialized).reverse() — this is how we know the next TX’s input in advance.

Cancel: payer spends the change output of the last broadcast TX. Since this spend has no locktime, it confirms immediately and invalidates all remaining pre-signed TXs.

Properties:

  • Standard P2PKH — no new opcodes, no OP_RETURN, works today
  • Wallet can be closed after creation — fully trustless
  • Receiver needs zero infrastructure — just holds raw TX hex and broadcasts on schedule
  • Cancellable at any time by the payer
  • Fees estimated upfront for the entire chain

Solution 2: Nostr-coordinated Invoices (Fixed USD — wallet online)

For USD-denominated subscriptions where the BCH amount changes with price:

  1. Payer authorizes: “pay $X to pubkey Y every N days”
  2. Merchant sends Nostr event (kind 22240 ) with invoice
  3. Payer’s wallet matches against stored subscriptions
  4. Fetches BCH/USD price, calculates sats, auto-signs, broadcasts
// Merchant sends invoice via NIP-04 encrypted DM
const invoice = {
  type: 'sub_invoice',
  sub_id: subId,
  amount_usd_cents: 500,  // $5.00
  period: 3,
  addr: merchantAddr
};
const encrypted = await nip04Encrypt(sessionPriv, payerPub, JSON.stringify(invoice));
const ev = await makeEvent(sessionPriv, 22240, encrypted, [['p', payerPub], ['t', '0penw0rld-sub']]);

Wallet must be open for this mode — but the payment logic is fully self-custody (keys never leave the client).

Settlement Checker (Receiver Side)

Runs every 30 seconds — same pattern as HTLC settlement:

async function checkSubSettlements() {
  for (const sub of subscriptions) {
    if (sub.status !== 'active' || sub.role !== 'receiver') continue;
    const now = Math.floor(Date.now() / 1000);
    for (const tx of sub.chain_txs) {
      if (tx.status !== 'pending' || now < tx.locktime) continue;
      try {
        await broadcast(tx.raw_hex);
        tx.status = 'broadcast';
        sub.periods_paid++;
      } catch (e) {
        // Input spent = payer cancelled
        if (e.message.includes('Missing inputs')) {
          sub.status = 'cancelled';
        }
      }
      break;
    }
  }
}

Comparison

Pre-signed Chain (BCH) Nostr Invoice (USD)
Currency Fixed sats Fixed USD (variable sats)
Wallet needed No (after creation) Yes (must be open)
Trustless Yes Semi (wallet auto-signs)
Cancel Spend change output Delete subscription
Infrastructure None Nostr relays
Opcodes Standard P2PKH Standard P2PKH

Open Questions

  • Any edge cases with nLockTime chain invalidation we might be missing?
  • Better fee strategies for long chains (12+ months)?
  • Is there interest in standardizing the Nostr event kinds (22240/22241) for cross-wallet compatibility?

Full implementation: sub.html buildSubscriptionChain() at line ~768