BNPL on Bitcoin: Buy Ordinals Now, Pay Later
A technical deep-dive into how I built Buy Now Pay Later for Ordinals at Liquidium.wtf, using atomic Bitcoin transactions and ICP canisters.
I recently built the BNPL (Buy Now Pay Later) system for Ordinals at Liquidium.wtf. Itβs now live with an integration on the Satflow marketplace. This post covers how it works from an implementation perspective.

The Problem
When someone wants to buy an Ordinal but doesnβt have the full BTC amount, they need a way to secure the asset now while paying later. This opens up speculative trading opportunities: buyers can acquire assets with minimal upfront capital, then decide whether to repay based on price movement. If the floor drops below their loan, they can walk away. If it rises, they repay and pocket the profit.
We needed a solution with specific constraints:
- No Custody: The asset shouldnβt be held by a trusted third party.
- Immediate Settlement: The seller must get paid immediately.
- Atomic Swap: The loan and purchase must happen in a single atomic action.
Architecture Overview
The system involves four main components:
- Marketplace (e.g. Satflow) handles user interaction, creates and stages the first transaction
- Liquidium.wtf Protocol handles validation and coordinates with the canisters
- Instant Loans Canister (ICP) validates loan terms and signs as lender
- BNPL Canister (ICP) stores the hidden transaction and handles atomic broadcast
The key insight is that we use two linked Bitcoin transactions that get broadcast together as a package. Transaction 1 (TX1) moves the Ordinal to an intermediate output. Transaction 2 (TX2) spends that output into escrow while paying the seller. Neither transaction is valid without the other.

The Flow
1. Marketplace Stages TX1
The marketplace creates and signs TX1, then calls store_collateral_tx directly on the BNPL canister. Their ICP principal is whitelisted, so only the marketplace can call this method:
#[update]
pub async fn store_collateral_tx(signed_tx_hex: String) -> Result<String, BnplError> {
is_admin().await?;
add_collateral_tx_to_storage(signed_tx_hex)
}
The canister:
- Validates the transaction structure (single input/output, version 3, non-zero value)
- Stores it along with the origin UTXO information
- Does NOT broadcast TX1 yet.
2. Buyer Prepares and Signs
The buyer selects an Ordinal on the marketplace and chooses BNPL. The protocol validates their funds, dummy UTXOs, and creates the TX2 PSBT. The buyer signs their inputs.
3. Instant Loans Canister Processes
When the buyer submits, the request goes to the Instant Loans canister which:
- Validates the loan terms (LTV, floor price, etc)
- Verifies the collateral transaction exists
- Signs TX2 as the lender
- Triggers the atomic broadcast
4. Atomic Broadcast
The BNPL canister handles the final step:
- Receives the broadcast request
- Fetches the stored TX1
- Validates that TX2 actually spends from TX1
- Submits both as a package using the
submitpackageRPC
This ensures both transactions enter the mempool together. Either both confirm or neither does.
Transaction Structure
TX1 (Collateral Preparation)
Created by the marketplace and stored in the BNPL canister before broadcast. It has exactly one input (the Ordinalβs current location) and one output (Marketplace multisig escrow).
Inputs: Outputs:
ββββββββββββββββββββ ββββββββββββββββββββ
β Origin UTXO βββββββββΊβ Collateral β
β (inscription) β β Output β
ββββββββββββββββββββ ββββββββββββββββββββ
The canister tracks the origin UTXO so we know exactly where the Ordinal came from.
You can see an example TX1 on-chain here: d23308eβ¦1ff3e68
TX2 (Loan Transaction)
Created by the protocol, this transaction spends TX1βs output and distributes funds to all parties:
Inputs: Outputs:
ββββββββββββββββββββ ββββββββββββββββββββββββββ
β Dummy 1 β β Dummy Consolidation β
ββββββββββββββββββββ€ ββββββββββββββββββββββββββ€
β Dummy 2 β β Escrow (ordinal here) β
ββββββββββββββββββββ€ ββββββββββββββββββββββββββ€
β Collateral βββ TX1:0 β Seller Payment β
ββββββββββββββββββββ€ ββββββββββββββββββββββββββ€
β Lender UTXOs β β Marketplace Fee β
ββββββββββββββββββββ€ ββββββββββββββββββββββββββ€
β Buyer UTXOs β β Initiation Fee β
ββββββββββββββββββββ ββββββββββββββββββββββββββ€
β Lender Change (opt) β
ββββββββββββββββββββββββββ€
β Buyer Change (opt) β
ββββββββββββββββββββββββββ
Because Ordinals are tracked by their specific satoshi offset, position in the transaction inputs matters. We inject βDummy UTXOsβ as padding inputs to shift the Ordinalβs index, ensuring the inscription lands exactly in the Escrow output and not in a change address or fee.
Both transactions use Bitcoin version 3 (TRUC/BIP-431), which enables package relay. This standard is supported by Bitcoin Core 28.0+ and major mining pools have adopted these mempool policies, making atomic package broadcasts reliable in practice.
You can see an example TX2 on-chain here: 6defa0eβ¦e1733e
PSBT Signing and Signature Types
TX2 requires signatures from four parties. The key to making this work is using specific signature hash types to let parties sign different parts of the transaction independently:
- Seller (TX1 & TX2 input): Signs with
SIGHASH_SINGLE | ANYONECANPAY. This commits only to their input and their payment output. They can sign early without knowing the buyer or loan details. - Buyer: Signs their payment inputs with
SIGHASH_ALL. - Liquidium (Lender): Signs lender inputs with
SIGHASH_ALLafter validating loan terms. - Marketplace: Signs the 2/2 multisig spend last to finalize the transaction.
The signing flow:
- Seller signs TX1 and TX2 (listing input only) with
SIGHASH_SINGLE | ANYONECANPAY - Buyer initiates purchase on marketplace
- Full TX2 PSBT is constructed
- Buyer signs with
SIGHASH_ALL - Marketplace signs the 2/2 inscription spend (completing the multisig)
- Liquidium signs lender inputs with
SIGHASH_ALLvia API call after validating PSBT - TX2 is finalized and package broadcast with TX1
Security Considerations
- TX1 stays hidden until broadcast: The BNPL canister holds the signed TX1 on ICP in encrypted storage that even Liquidium cannot access. The ICP runtime ensures data privacy until the canister explicitly reveals it during broadcast. This prevents front-running where someone sees TX1 in the mempool and acts before the loan finalizes.
- Origin UTXO tracking: The BNPL canister tracks exactly where the Ordinal came from, allowing validation that the correct inscription is being used.
- TRUC constraints: Version 3 transactions enable package relay, preventing replacement and pinning attacks. The 1-parent-1-child topology restriction also limits the attack surface for transaction pinning.
- Inter-canister authorization: Only whitelisted canisters and admin ICP principals can interact with the BNPL canister.
- Atomic dependency: TX2 explicitly spends TX1βs output. If TX1 isnβt present, TX2 is invalid. The package broadcast ensures atomicity.
What Happens After
Once both transactions confirm:
-
The seller has their BTC
-
The Ordinal sits in liquidium loan escrow
-
The buyer has a loan with a fixed expiry (7-30 days)
-
Repayment: If the buyer repays before expiry, the Ordinal releases to them.
-
Default: If they donβt, the lender claims it. The buyer loses their down payment.
Plugging Into Existing Liquidity
A major advantage is that BNPL uses Liquidiumβs existing liquidity pool without requiring new lender infrastructure:
- Unified Liquidity: Lenders providing BTC for standard loans automatically support BNPL.
- Same Risk Profile: From a lenderβs perspective, itβs just another Ordinal-backed loan.
- Automatic Matching: Liquidium handles matching and signing identical to standard loans.
Want to try it out? Check out BNPL on Satflow.