Skip to content

Listing

A listing sells one unit that sits alone on its own attached asset UTXO. The seller signs that UTXO with SIGHASH_SINGLE|ANYONECANPAY (0x83) against a payment output worth the price. A buyer later completes that signature into a checkout transaction.

You need the seller’s session and a signer that can sign the returned PSBT input with SINGLE|ANYONECANPAY. Listing a prepared unit needs no new Bitcoin transaction; preparing a detached balance does.

Choose the path that matches your inventory:

Path When Bitcoin transactions
List prepared inventory The unit is already on its own asset UTXO None: listing broadcasts nothing
Attach-and-list The unit is still a detached address balance One attach, then the listing activates after it confirms
  1. Read prepared inventory. It needs no session and returns exact outpoints.
  2. POST /v1/listing-preflights with an idempotency_key, seller, asset, asset_utxo: {txid, vout}, and price_sats.
  3. Require mode: "reuse", status: "ready", and the same outpoint and price.
  4. Sign listing.psbt_hex: only listing.inputs_to_sign, with SINGLE|ANYONECANPAY. Do not sign inputs outside that instruction.
  5. POST /v1/listing-preflights/:id/listing with psbt_hex and expires_at (Unix seconds, or null for no marketplace expiry).
Terminal window
curl -s "$API/v1/address/bc1q.../inventory/prepared?asset=RAREPEPE"
curl -s -X POST "$API/v1/listing-preflights" \
-H 'content-type: application/json' -H "x-session-token: $TOKEN" \
-d '{
"idempotency_key": "inventory-001-0",
"seller": "bc1q...",
"asset": "RAREPEPE",
"asset_utxo": { "txid": "aaaa...aaaa", "vout": 0 },
"price_sats": 100000
}'
curl -s -X POST "$API/v1/listing-preflights/$PREFLIGHT_ID/listing" \
-H 'content-type: application/json' -H "x-session-token: $TOKEN" \
-d '{"psbt_hex":"70736274ff...","expires_at":null}'

Providing asset_utxo is a prepared-only instruction: an unavailable output is refused, never replaced and never attached again. The idempotency key binds the seller, asset, price, and outpoint, so repeating the exact request after a lost response returns the same preflight.

An unsigned draft of a prepared unit holds nothing: a new draft at another price may name the same unit, and the first signed listing wins (a later signature on the other draft is refused). Without asset_utxo, the server picks units no live draft names first, so several drafts opened together get different units. Before signing you can also change a draft’s price with POST /v1/listing-preflights/:id/price, or drop it with POST /v1/listing-preflights/:id/discard (it does not detach anything).

preflight.next_action tells you what the preflight is waiting for: sign_attach, wait_for_indexer, set_price, sign_listing, list_again (status withdrawn: a reorg withdrew the listing and the attach is mined again, so list the attached unit again with a new preflight), or null when it is finished.

Preparation attaches one unit per transaction (raw quantity 1 for an indivisible asset, 100000000 for a divisible one). There is no server-side preparation job: the server composes and checks, you sign and relay.

  1. GET /v1/address/:address/inventory/attachable for detached balances.
  2. POST /v1/attach-templates with utxo_owner, asset, and optionally asset_source, fee_rate, exclude, in_flight, and funding. It returns one verified one-unit attach PSBT, its signer groups, the change output, and a fee quote. It writes nothing and reserves nothing.
  3. Sign it, then relay the finalized bytes with POST /v1/transactions (raw_tx_hex).
  4. Track it yourself. POST /v1/transactions/status checks up to 25 txids; final means at least 3 confirmations.

Keep your own state per attach, because the server keeps none:

  • Pass the inputs you already spent in earlier attaches as exclude, so server coin selection skips them.
  • Declare your signed-but-not-final attaches as in_flight (asset, raw units, and asset_utxo once known). The server only ever uses this to refuse composing units those attaches already use.
  • To prepare several units from one coin without waiting for a block, fund each attach from the previous one’s change: pass the template’s change object (txid, vout, value_sats, script_pub_key_hex) back as funding: [change]. Without explicit funding the server selects only confirmed coins, and answers 422 with reason: "no_funding" and a pending_confirmation count while yours are still unconfirmed.
  • Save signed bytes before you broadcast. An uncertain relay retries the exact same bytes; it never re-signs.

If an attach waits in the mempool because fees rose, pay more from its own change with a child transaction (CPFP). The server keeps nothing for this either:

  1. POST /v1/attach-templates/speed-up with owner (your session address), txid (the attach), and optionally fee_rate (default: the next-block rate). For a chain of attaches, pass the newest one: its change is the unspent one, and the child is sized over every unconfirmed ancestor.
  2. Check result.child: one input (the attach’s change, never output 0, the asset UTXO), one output back to you worth final_change_sats, transaction version 2, locktime 0. fees.package_fee_rate is what the attach, its unconfirmed ancestors, and the child pay together.
  3. Sign input 0 (DEFAULT|ALL), finalize, and relay it with POST /v1/transactions. Do not spend that change again in a later attach.

A refusal names its reason: confirmed, fee_sufficient (already pays the rate), change_spent (with spent_by: speed up that transaction instead), no_change, ancestor_limit (Bitcoin’s 25-transaction chain limit), or insufficient_change (insufficient_funds with need_sats and have_sats).

After signing and finalizing the child locally, verify that its txid matches result.child.expected_txid before relaying it.

The source pays the Bitcoin fee and Counterparty’s attach fee in XCP. The XCP figure is a quote until the attach is parsed; do not present it as a miner or marketplace fee. An attach that cannot be paid for fails with insufficient_asset_balance or insufficient_funds, whose bodies say exactly what is short.

Omit asset_utxo from POST /v1/listing-preflights and the API chooses. If the seller already holds an unlisted, verified one-unit asset UTXO of that asset, it reuses it (mode: "reuse", status: "ready": sign the listing as above and nothing broadcasts). Otherwise it composes an attach for you (mode: "attach", status: "awaiting_attach_signature"). Branch on mode; pass asset_utxo when you need a specific unit. An attach response carries an attach signing instruction and the dependent listing template. The two are shaped differently: the listing says listing.inputs_to_sign, while the attach names its signers, because a legacy asset_source may sign some of its inputs:

"attach": {
"psbt_hex": "70736274ff...",
"signers": [{ "address": "bc1p...", "inputs_to_sign": [0] }],
"sighash": "ALL",
"expected_txid": "0d9a..."
}

Sign only the groups whose address you control. For witness-only inputs, check the finalized txid against expected_txid. A P2PKH source can change the attach txid during signing; use the final outpoint returned after submission. POST /v1/attach-templates returns the same attach object.

  • Sign both and send them together to POST /v1/listing-preflights/:id/attach-and-list (attach_psbt_hex, listing_psbt_hex, expires_at). Only the attach broadcasts now.
  • Or sign the attach alone, POST /v1/listing-preflights/:id/attach, poll GET /v1/listing-preflights/:id until status is ready, then sign and submit the listing.

The listing becomes public only after the attach confirms on Bitcoin, Counterparty binds the exact outpoint, and the actual XCP fee is reconciled. The API stores the signed attach before broadcasting and rebroadcasts it on its own if needed.

Repricing needs a fresh signature, not another attach.

Review result.current_price_sats and the new result.listing template. Verify the PSBT against the new price, then sign only listing.inputs_to_sign with SINGLE|ANYONECANPAY and submit the signed PSBT.

Routes: POST /v1/listings/:id/reprice-preflight (price_sats), then POST /v1/listings/:id/reprice (psbt_hex). The preview shows the current price, but it is not a lock: do not run two repricers against the same inventory.

POST /v1/listings/:id/delist with the owner’s session. No signature needed.

Terminal window
curl -s -X POST "$API/v1/listings/$LISTING_ID/delist" -H "x-session-token: $TOKEN"

Neither delist nor reprice can overwrite a settlement already in progress; you get a conflict instead.

POST /v1/listings/batch is the collection-level form of the per-listing routes. It takes an action and 1..20 distinct listings, every one sold by the session address, and runs each item through the single route’s own checks.

Action Body Answer
reprice_preflight listings: [{listing_id, price_sats}] {listings: [...]}, one reprice template per listing, in order
reprice listings: [{listing_id, psbt_hex}] {listings: [{listing_id, ok: true, price_sats}]}
delist listings: [{listing_id}] {listings: [{listing_id, ok: true}]}

For each returned preflight, verify the new price and sign listing.inputs_to_sign with SINGLE|ANYONECANPAY. Submit the resulting {listing_id, psbt_hex} entries together with action: "reprice".

  • The reprice preflight and submit are validated whole: if any item is refused (not yours, not active, a counter, same price, below the floor, spent, or a signature that fails), the batch is refused with the first refusal’s status and code, and refused_listings: [{index, listing_id, status, code, error}] lists every refused item. Nothing is written.
  • A buyer who returned a valid signature between verification and write keeps priority: that item comes back {ok: false, code: "state_changed"} and the others are repriced.
  • The batch delist refuses whole only for a missing (404) or foreign (403) listing. One that already sold, expired, was delisted, or is mid-purchase comes back {ok: false, code: "state_changed"}; the rest are delisted.
Route Session Returns
GET /v1/address/:address/listings no Public active listings for a seller
GET /v1/address/:address/managed-listings owner Active and pending listings, including private counters
GET /v1/address/:address/ended-listings owner Listings that ended in the last 30 days, each with a reason
GET /v1/address/:address/listing-preflights owner Your preflights

Use preflights for both attached inventory and new attaches. They give you idempotent creation and server-side recovery.

Bulk listing is a set of independent orders, not an atomic batch. Some can succeed before a later one is rejected.