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

There’s a problem with liquidity here. If you want to support monthly recurring payments for 1 year, you have to lock up 1 year’s worth of BCH ahead of time. This is essentially a payment channel where recipient can settle parts of it as pieces mature and you can close the channel by spending the UTXO to yourself. But your liquidity is locked. If you want to take some BCH out, you will invalidate ALL pre-signed TXs and will have to interact with your recipient again.

Not anymore. Now you have CashTokens and TX introspection opcodes. It’s possible to put your BCH in a little smart contract pool, and the pool can emit “authorized to pull X amount every N blocks or seconds” NFTs to recipients. Here you can share the same liquidity pool with many services. As long as you maintain balance, all the services can keep pulling their subscriptions. If your balance drops, it can be seen as cancellation or pausing. If you encumber the authorization NFTs with a covenant, you could explicitly revoke subscriptions by burning their NFTs.

Anyway, I think the “pre-signed chain” is too fragile, and liquidity locking is a real problem that would hamper adoption of this.

1 Like

Really nice work especially appreciate the decision to stay within standard P2PKH and avoid OP_RETURN or new opcode assumptions. The pre-signed chain approach is one of the few designs that fully respects BCH’s current constraints while still achieving deterministic scheduling.

The insight around “you can know future UTXOs if you create them yourself” is particularly elegant it sidesteps the usual uncertainty around future outputs in a clean way.

One thing that stood out to me is how the model effectively encodes time-based intent into a fixed transaction graph. That gives you strong guarantees (offline wallet, no infrastructure), but also introduces rigidity:

  • The payment schedule is fully pre-committed
  • Any modification (amount, frequency, partial withdrawal) breaks the chain
  • Each subscription is isolated to its own UTXO flow

So it feels like this approach optimizes for determinism and simplicity , but at the cost of flexibility and capital efficiency especially as duration increases.

The Nostr-based approach goes in the opposite direction (more dynamic, but requires liveness), which makes the contrast between the two models really interesting:

  • one is pre-authorized execution
  • the other is continuous authorization

It almost feels like these are two ends of a spectrum, and the missing middle is a model where:

  • funds remain in a shared pool
  • but spending conditions are enforced at the script level rather than via pre-signed sequences

Curious how you think about that tradeoff long-term do you see pre-signed chains as a practical production model, or more of a stopgap until more expressive covenant-style patterns are widely adopted?

Also +1 on standardizing the Nostr event kinds without that, interoperability across wallets will be messy very quickly.

2 Likes

Nice work especially getting both approaches live within standard P2PKH constraints.

We’ve been working on streaming-style payments on chipnet as well, and one thing we noticed when experimenting with pre-signed chains is how much the model depends on strict determinism during construction .

A few things that came up on our side:

  • Tx construction needs to be fully controlled
    Since each step depends on the exact txid of the previous one, even small differences in fee calculation or output ordering can invalidate the rest of the chain. We ended up treating chain construction as a “closed process” rather than relying on typical wallet abstractions.

  • Change output shrink over long schedules
    As the chain progresses, the remaining output gets smaller, so over longer durations you start running into edge cases around dust thresholds or transactions becoming uneconomical relative to fees.

  • Time behavior is slightly non-intuitive
    Because nLockTime uses median-time-past, transactions don’t become valid at an exact timestamp but rather “after a threshold,” which can introduce small timing drift depending on block conditions.

  • Dependency on prior confirmations
    Since each tx spends the previous one, anything that affects confirmation of an earlier step (e.g. reorgs or delays) propagates forward.

On the Nostr side, we’ve looked at similar coordination patterns, and the main tradeoff we saw is that execution guarantees move from the chain to the client layer so correctness depends more on wallet uptime and message delivery than purely on script validity.

Overall, I think this is a really solid exploration of what can be achieved without new primitives.

From our experience, once you start pushing toward:

  • multiple concurrent streams
  • or longer-lived payment schedules

you begin to feel the limits of stateless transaction chaining, and the problem shifts toward how to maintain shared, enforceable state across payments.

Curious how these approaches behave under longer horizons or more complex subscription sets * that’s where most of the interesting edge cases showed up for us.

I don’t understand why both of you are spinning this as if it is some benefit. It only makes the product worse than it could be if you actually used available opcodes and native token features. Pre-signed chains are fragile and capital inefficient. There’s a reason P2PKH-based Flipstarter moved to smart-contract-based https://fundme.cash

It is 2026. BCH Script is far more advanced than BTC’s, and now you have a whole palette of efficient smart contract primitives and new opcodes, instead of having to rely on pre-2016 ways. These are not “assumed opcodes” - they have been active for almost 3 years now.

Be sure to check out existing mainnet automaton contracts for recurring payments, which have been in production since circa 2023: Unspent Phi
They have been designed as irrevocable, but it’s not hard to edit them and add an admin key spending path so you can cancel or change balance, it is fairly simple CashScript.

It’s very simple: spender generates a contract and funds it, receiver magically gets paid: because the contracts can be executed by anyone as soon as conditions allow the next TX. (and they claim a little “executor fee” for this 3rd party service).
Yes, because of how P2SH works contracts have to be announced with an OP_RETURN marker because spender needs to learn the redeem script somehow.

But not for long, in May 2026, P2S upgrade will activate and allow non-standard locking scripts, so no more need for OP_RETURN: just put the contract parameters in the bare script for everyone to see, and then they can find these “anyonecanspend” convenants more easily, and it becomes an open market for contract execution.