Sale PSBT Construction

The transaction-level reference for snipe-protected sales: the two PSBTs a seller signs at listing time, the package a buyer broadcasts at sale time, and the script that makes both self-custodial.

Updated · Plain-language version: how snipe protection works

Overview

Three transactions matter. The seller signs the first two at listing time and pays nothing; nothing is broadcast until a buyer appears. The third exists only as an escape hatch.

The three transactions of a protected sale
LabelTransactionSigned byBroadcast
TX1Passthrough: inscription UTXO → escrow outputseller, at listingnever alone; as the parent of TX2
TX2Sale template: escrow → seller payoutseller at listing; co-signer at purchaseby the buyer, inside a package with TX1
TX3Recovery: escrow → seller, 144 blocks after TX1 confirmsseller, on demandby the seller, only if TX1 ever confirmed without a sale
One purchase link: TX1 rides as the parent of TX2, and the seller's SIGHASH_SINGLE signature ties escrow input k to payout output k.

Listing

Listing costs the seller nothing and produces no broadcast. The client builds TX1 and TX2 from the server's template, verifies every field against pinned constants, and asks the wallet for two signatures in one prompt.

TX1: passthrough

Fields of the passthrough transaction
FieldValue
version2
locktime0
input 0the inscription UTXO, sequence 0xffffffff
output 0postage − fee sats to the escrow script
fee0 sats, or 12 sats (0.1 sat/vB at 111 vB) when postage − 12 ≥ 330 and the inscription's sat offset is below postage − 12

The postage moves whole and the inscribed sat keeps its offset, so an inscription sitting at the bottom of its UTXO is as safe as one at the top. A zero-fee transaction cannot confirm on its own; it confirms only as the parent of a fee-paying TX2, so an escrow can never be stranded by a listing that no one buys. The 12-sat variant exists so that relays which enforce a 0.1 sat/vB floor without package evaluation, such as mempool.space, still accept the parent.

PSBT fields the client sets on input 0 before signing: witness_utxo, tap_internal_key (the seller's key for a native Taproot input) and sighash_type = SIGHASH_DEFAULT. The client rejects a template whose input, output count, fee or destination script differ from the rules above.

TX2: sale template

Fields of the sale template
FieldValue
version2
locktime0
input 0TX1 output 0 (the escrow), sequence 0xffffffff
output 0exactly the list price, to the seller's payout address

The seller signs input 0 on the sale leaf with SIGHASH_SINGLE | SIGHASH_ANYONECANPAY (0x83, a 65-byte signature). That commits to “this escrow pays me this amount at this index” and to nothing else, so the signature can be dropped into any larger transaction that also carries the co-signature, at any input position whose output index matches.

PSBT fields on input 0: witness_utxo, tap_internal_key = NUMS, tap_merkle_root, tap_scripts = [(sale leaf, control block)], sighash_type = 0x83. The internal key is the BIP-341 unspendable point, so a wallet that prefers key-path signing has no key path to fall back to and produces a leaf signature like everyone else.

The server stores the two signatures and the escrow price. Repricing replaces TX2's signature only; TX1 is unchanged. Delisting deletes both. Nothing touches the chain.

Recovery path

TX3 is not presigned. If TX1 ever confirms without a sale (a buyer's package was mined with a different link, or a relay accepted the 12-sat parent on its own), the seller requests a recovery PSBT that spends the escrow through the recovery leaf. Its single input carries sequence = 144, so it is valid 144 blocks (about one day) after TX1 confirmed, and needs only the seller's key.

Sale

A purchase of n items is a chain of n single-item sales, each broadcast as a two-transaction package [TX1_i, TX2_i]. Link i (0-based) has these inputs and outputs; k is the number of leading buyer inputs, so the escrow sits at input k and the payout at output k.

Inputs of one sale link
InputWhatSigned with
0..k−1buyer funding (link 0: the buyer's own UTXOs; later links: change output 0 of the previous link)buyer, SIGHASH_DEFAULT
kescrow of item i (TX1_i output 0)seller 0x83 on the sale leaf + co-signer SIGHASH_DEFAULT on the same leaf
k+1..optional trailing fee inputs (later links: the last change output of the previous link)buyer, SIGHASH_DEFAULT
Outputs of one sale link
OutputWhatValue
0change 1, to the buyer's funding addressΣ leading inputs − price; funds the next link
kseller payoutexactly the list price, at the escrow's input index
k+1the inscription, to the buyer's receive addressthe escrow's full value
nextmarketplace fee (link 0 only)2% of the basket, minimum 1,000 sats
next…creator royalties (link 0 only)per collection policy
lastchange 2, to the buyer's funding addressremaining fee funding, when above dust

The seller's payout index equals the escrow's input index because of SIGHASH_SINGLE; every other output is under the buyer's control. The inscription output reuses the escrow's postage, so the sat offset is again unchanged.

Fee and size rules for a purchase
RuleValue
fee per linkexact signed size of TX2_i plus 111 vB for TX1_i, at the chosen rate
minimum rate1 sat/vB, plus 2 sats
links per purchaseup to 12
unconfirmed ancestors of link i2i, inside the 25-ancestor relay limit
setup transactionbuilt first when the wallet cannot split its UTXOs: at most 8 inputs, two outputs (Σ price + 330 and the rest)

The buyer signs every link and submits them in order. The server verifies each link byte-for-byte against its own template, adds the co-signature (the co-signer key is used only here, never at build time), finalizes the escrow witness [cosigner_sig, seller_sig, sale leaf, control block], checks every input under consensus rules, and relays with submitpackage([TX1_i, TX2_i]). The parent is never sent alone, and a package our node does not accept is reported to the buyer as failed. The package is then relayed best-effort to mempool.space's package endpoint and other public nodes.

Package relay

There are no anchor UTXOs and no venue-funded fee bumps. TX1 pays no fee, so nothing in the mempool can be replaced to steal the item: a competing buyer would need their own link carrying the same seller signature and our co-signature, and the server co-signs a given escrow for at most one armed sale at a time, with at most 12 armed chains per buyer address. If a buyer's package lingers at a low fee rate, the buyer can rebuild at a higher rate; if it never confirms, the listing simply stays live.

Passthrough Tapscript

The escrow's Taproot tree. Green is the sale path; grey is the seller-only recovery path.
tr(
  NUMS,
  {
    <S> OP_CHECKSIG <C> OP_CHECKSIGADD 2 OP_NUMEQUAL,        # sale leaf
    144 OP_CHECKSEQUENCEVERIFY OP_DROP <S> OP_CHECKSIG        # recovery leaf
  }
)
  • S is the seller's x-only key; C is the Ordinals Wallet co-signer key, pinned in the client.
  • Sale leaf: seller and co-signer together. The co-signer key alone can move nothing; with a compromised API it can at most complete a sale at the listed price to a buyer who paid it, or refuse to sign.
  • Recovery leaf: seller alone, 144 blocks after the escrow confirms.

Both the client and ord's sale classifier reconstruct this tree from the seller's key and the pinned co-signer key, so a template whose escrow script does not match is rejected before the wallet is asked to sign.

Verify it yourself

Two constants and one mined purchase are enough to check every claim on this page against the chain.

Ordinals Wallet co-signer key (x-only)
1d08b7c71f6f1e97a0a4cf005db7a977c85e34652a0c9365842aee25997c7deePinned in the app. A listing or purchase whose escrow uses any other key is refused before signing.
Internal key (BIP-341 NUMS)
50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0The unspendable point from BIP-341. It guarantees there is no key-path spend.
A two-item protected purchase mined on mainnet
LinkTransactionNote
0c8dce131ec8be3d0block 966,268, zero-fee parent, opens in a new tab
19532cc0c5b261473spends change 1 of link 0, zero-fee parent, opens in a new tab