BchainPay logoBchainPay
EngineeringBitcoinPaymentsSecurityRisk

Bitcoin RBF Explained: What Full-RBF Means for Crypto Merchants

How BIP-125 opt-in RBF and Bitcoin Core full-RBF affect merchant payments, plus recommended confirmation thresholds and BchainPay webhook patterns for safe BTC receipt.

By Cipher · Founding engineer, BchainPay9 min read
Illustration for Bitcoin RBF Explained: What Full-RBF Means for Crypto Merchants

The Bitcoin mempool is not a queue. It is an open auction where any sender can broadcast a higher-fee replacement for their own unconfirmed transaction, re-routing the coins to a different address before a single block is mined. For merchants, that window between "I see the transaction" and "the transaction is confirmed" is where fraud lives.

Replace-by-Fee (RBF) is the mechanism that governs when replacement is allowed. BIP-125 formalized it in 2015. The Bitcoin Core 24.x shift toward full-RBF changed the rules again. This post covers both, explains the attack surface they create for merchants, and shows the BchainPay confirmation_policy knobs that make BTC payments safe.

How RBF works: BIP-125 nSequence signaling#

A Bitcoin transaction signals replaceability in its inputs. Each input carries a 4-byte nSequence field. BIP-125 defines a transaction as opt-in replaceable if any input has nSequence <= 0xFFFFFFFD (decimal 4,294,967,293). Most wallets that want RBF set nSequence to 0x00 or 0xFFFFFFFD; wallets that do not want RBF set it to 0xFFFFFFFE or 0xFFFFFFFF.

Replaceability is also inherited. A transaction that does not signal RBF in its own inputs is still replaceable if one of its unconfirmed ancestors does. Your mempool entry's RBF status must therefore be evaluated relative to the full ancestry chain, not just the immediate transaction.

When a replacement is broadcast, a node accepting it checks five BIP-125 rules:

  1. The replacement itself signals replaceability.
  2. Every input present in the original is still present in the replacement.
  3. The replacement does not pull in additional unconfirmed inputs that were not already in the original.
  4. The replacement pays an absolute fee at least as large as the original.
  5. The replacement pays an incremental fee sufficient to cover relay at the node's minimum fee rate (1 sat/vByte under default policy, but this is a per-node parameter).

Miners keep whichever version pays the higher fee-per-vByte. The other is evicted.

Check replaceability for an unconfirmed transaction via Bitcoin Core's getmempoolentry RPC — it returns a boolean:

bitcoin-cli getmempoolentry "b3c14f7a..."
{
  "fees": { "base": 0.00001234, "modified": 0.00001234 },
  "vsize": 141,
  "bip125-replaceable": true,
  "ancestorcount": 1,
  "descendantcount": 1
}

getmempoolentry throws if the transaction is no longer in the local node's mempool (either confirmed elsewhere or evicted). The gettransaction wallet RPC also exposes replaceability as a three-way string: "yes", "no", or "unknown". "unknown" indicates an unconfirmed ancestor is replaceable and the flag is inherited rather than set directly.

Full-RBF: what changed in Bitcoin Core 24.x#

Bitcoin Core 24.x shipped a new mempool configuration option: -mempoolfullrbf. When enabled on a node, that node will relay and accept replacement transactions regardless of whether the original set the BIP-125 signal. No opt-in required from the sender.

Full-RBF is a relay policy change, not a consensus rule change. Confirmed transactions are unaffected. What changes is the unconfirmed period: a well-resourced attacker can now broadcast a replacement via full-RBF-enabled relay nodes and reach miners without needing to have signaled BIP-125 in the original.

As more mining pool infrastructure has enabled -mempoolfullrbf, the practical implication is this: bip125-replaceable: false is now an informational signal, not a safety guarantee. A transaction without the RBF flag was once protected from relay-level replacement by default node policy; that protection is weaker now and should not be relied upon for payment acceptance decisions.

Two things that full-RBF does not change:

  • Confirmed transactions remain irreversible modulo the usual reorg risk.
  • The cost of a double-spend attack is not zero. The attacker still needs to pay a higher fee and race the replacement to miners before the original confirms.

Attack patterns#

Fast double-spend via RBF#

  1. Customer broadcasts tx_A: 0.012 BTC to the merchant's deposit address, RBF-signaled, low fee.
  2. Merchant's checkout detects the unconfirmed transaction and releases goods.
  3. Customer immediately broadcasts tx_B: same UTXOs, coins redirected to their own address, higher fee.
  4. Miners confirm tx_B. tx_A is evicted from the mempool. The merchant received nothing.

The attack is cheap to attempt and requires no miner collusion. The payment_intent.detected event BchainPay emits when a transaction first hits the deposit address is informational only. Goods should never be released on detected alone. The only reliable defense is confirmation depth.

Pinning: a liveness risk, not a theft risk#

In the reverse scenario, a customer intentionally sends a very-low-fee non-RBF-signaling transaction to clog the merchant's intent without confirming. The transaction sits in the mempool indefinitely. This is more a UX disruption than a theft attack (the customer also cannot spend those coins elsewhere), but it delays fulfillment and strands the intent.

CPFP is the merchant's tool here — described in the last section.

Confirmation depth is the only reliable finality signal for Bitcoin. The following thresholds are a reasonable starting policy; adjust based on your fraud tolerance and fulfillment latency requirements:

Amount tier Recommended blocks Approx. wait
< $50 1 ~10 min
$50–$500 2 ~20 min
$500–$2,000 3 ~30 min
> $2,000 6 ~60 min

The 6-block bar for high-value transactions is the longstanding industry standard. At current network hash rates, mounting a successful double-spend requiring 6 consecutive blocks is economically infeasible for any realistic payment amount. One confirmation eliminates mempool-level RBF attacks and is appropriate for low-value digital goods.

Configuring BchainPay payment intents for BTC#

Set confirmation_policy when creating the intent:

const res = await fetch('https://api.bchainpay.com/v1/payment-intents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BCHAINPAY_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: '1200.00',
    currency: 'USD',
    accept: ['BTC'],
    confirmation_policy: {
      bitcoin: { blocks: 3 },
    },
    confirmation_timeout: 7200,
    expires_in: 7200,
    metadata: { order_id: 'ord_7719' },
  }),
});
const intent = await res.json();

The named preset "conservative" maps to { bitcoin: { blocks: 6 } } and is appropriate for physical goods or anything you cannot revoke. "fast" maps to { bitcoin: { blocks: 1 } } for low-value digital goods.

Webhook states to handle#

Every BTC payment intent passes through the same four event types. Fulfill only on payment_intent.succeeded:

switch (event.type) {
  case 'payment_intent.detected':
    // Transaction is in the mempool. Do NOT fulfill.
    // Show "payment detected, awaiting confirmation" to your customer.
    await db.orders.updateStatus(orderId, 'payment_detected');
    break;
 
  case 'payment_intent.succeeded':
    // Configured block depth reached. Safe to fulfill.
    await fulfillOrder(orderId, event.data);
    break;
 
  case 'payment_intent.invalidated':
    // The original transaction was replaced (RBF) or the block re-orged.
    // Roll back any provisional state.
    await cancelProvisionalFulfillment(orderId);
    await notifyCustomer(orderId, 'payment_reversed');
    break;
 
  case 'payment_intent.timed_out':
    // confirmation_timeout elapsed before the required depth was met.
    await db.orders.updateStatus(orderId, 'payment_timeout');
    await offerAlternativePayment(orderId);
    break;
}

payment_intent.invalidated covers both RBF replacements (the original transaction evicted before confirming) and block reorgs deep enough to undo blocks that had already cleared the threshold. Handle both the same way: roll back provisional state and flag the order for review.

The payment_intent.succeeded payload includes the final confirmation data:

{
  "type": "payment_intent.succeeded",
  "data": {
    "id":                    "pi_01HZ...",
    "chain":                 "BTC",
    "tx_hash":               "b3c14f...",
    "amount_received_btc":   "0.01234567",
    "amount_received_usd":   "1197.38",
    "confirmations":         3,
    "required_confirmations": 3,
    "block_height":          895412
  }
}

Detecting RBF status in your own Bitcoin node#

If you run a Bitcoin Core node alongside BchainPay for independent audit or monitoring, here is a TypeScript function that classifies an unconfirmed transaction's replaceability:

import { BitcoinRpcClient } from './btc-rpc';
 
type RbfStatus =
  | 'confirmed'
  | 'signaled'          // explicit BIP-125 opt-in
  | 'inherited'         // unconfirmed ancestor is signaled
  | 'not-signaled'      // no BIP-125 flag — informational, not a safety guarantee
  | 'evicted';          // no longer in mempool
 
async function classifyRbf(
  rpc: BitcoinRpcClient,
  txid: string,
): Promise<RbfStatus> {
  // gettransaction works for watch-only wallet addresses
  try {
    const tx = await rpc.call<{
      confirmations?: number;
      'bip125-replaceable': 'yes' | 'no' | 'unknown';
    }>('gettransaction', [txid, true]);
 
    if ((tx.confirmations ?? 0) >= 1) return 'confirmed';
 
    switch (tx['bip125-replaceable']) {
      case 'yes':     return 'signaled';
      case 'unknown': return 'inherited';
      case 'no':      return 'not-signaled'; // not safe under full-RBF
    }
  } catch {
    // Not in wallet — fall through to getmempoolentry
  }
 
  // getmempoolentry works for any in-mempool transaction (boolean field)
  try {
    const entry = await rpc.call<{
      'bip125-replaceable': boolean;
    }>('getmempoolentry', [txid]);
 
    return entry['bip125-replaceable'] ? 'signaled' : 'not-signaled';
  } catch {
    return 'evicted';
  }
}

Important: evicted means the transaction is no longer in the local mempool. This can mean a replacement confirmed, the transaction was pruned after the default two-week expiry, or your node briefly went out of sync. Always cross-check with payment_intent status via the BchainPay API before acting on an evicted result.

not-signaled does not mean safe. Under full-RBF relay, treat it as equivalent to signaled for payment policy purposes.

CPFP vs RBF: the merchant's toolkit#

These two mechanisms serve different purposes and are often confused:

Technique Controlled by Purpose
RBF Sender only Replace or fee-bump an unconfirmed outbound transaction
CPFP Recipient Accelerate a stuck inbound transaction by spending from the unconfirmed output

CPFP does not protect against double-spend. If a sender RBFs the original transaction before your CPFP child confirms, both the parent and the child are invalidated. CPFP is useful when a low-fee deposit is pinned in the mempool and you need it mined faster without the sender's cooperation.

BchainPay applies CPFP internally on hot-wallet sweeps when a deposit stalls. For outbound BTC payouts via /v1/payouts, pass fee_strategy: "rbf" to sign the output transaction as opt-in replaceable. If the payout stalls, bump the fee:

POST /v1/payouts/po_01HZ.../fee-bump
Content-Type: application/json
Authorization: Bearer sk_live_...
 
{ "target_fee_rate_sat_vb": 22 }

BchainPay broadcasts a properly signed RBF replacement at the new fee rate. The payout status advances from pending to broadcasting once the replacement propagates.

Key takeaways#

  • Any input with nSequence <= 0xFFFFFFFD signals BIP-125 replaceability. Replaceability is also inherited from unconfirmed ancestors.
  • Bitcoin Core 24.x introduced -mempoolfullrbf, which lets opt-in nodes relay replacements for non-signaling transactions. bip125-replaceable: false is now an informational signal, not a safety guarantee.
  • Zero-conf BTC is not safe for merchant fulfillment. One block confirmation eliminates mempool-level RBF attacks. Six blocks is the standard for orders above $2,000.
  • Set confirmation_policy: { bitcoin: { blocks: N } } in your BchainPay payment intent to match your order value and fulfillment latency.
  • Fulfill only on payment_intent.succeeded. The detected event is informational. invalidated covers RBF replacements and reorgs alike.
  • CPFP accelerates confirmation; it does not prevent double-spend. Only block depth is a reliable fraud defense on Bitcoin.

Try it yourself

Spin up a sandbox merchant in under 60 seconds.

One REST endpoint, signed webhooks, five chains. No credit card required.

Related reading